Sindbad~EG File Manager

Current Path : /opt/nginxhttpd_/etc/openresty_config/lua/lib/
Upload File :
Current File : //opt/nginxhttpd_/etc/openresty_config/lua/lib/o2switch_js_challenge_v2.lua

--[[
    Improved JS Challenge
--]]

local _M = {}

local o2debug = require "lib/o2switch_debug"
local o2config = require "lib/o2switch_config"
local o2redis = require "lib/o2switch_redis"
local o2obfuscate = require "lib/o2switch_obfuscate_js"
local o2utils = require "lib/o2switch_utils"
local hashWrapper = require "lib/o2switch_hash_wrapper"
local json = require "cjson"
local md5 = ngx.md5
local today = ngx.today
local time = ngx.time
local cookieTime = ngx.cookie_time
local salt = o2config.cookieSecret
local table = table
local math = math
local string = string
local tostring = tostring
local pcall = pcall
local type = type
local pairs = pairs
local gsub = string.gsub

local CHALLENGE_REDIS_TTL = 120 -- The redis TTL for the challenge response. After that, it's deleted.
local ONGOING_CHALLENGE_SUFFIX = ':chl:o' -- Redis key suffix used to store ongoing challenge data

-- Those will never be used by the js challenge, so we can search replace for them just 1 time
local function removeUselessPattern(challengeTemplate)
    return ((challengeTemplate:gsub('#(%w+)', {
        ['chlType'] = 'js',
        ['captchaKey'] = '',
        ['captchaProvider'] = '',
        ['captchaProviderJsInclusion'] = '',
        ['captchaDiv'] = '',
    })))
end

-- Local copie for the performance boost
local challengeTemplate = removeUselessPattern(challengeTemplate)

-- Running a gsub on multiple pattern on the template is not efficient and king of slow
-- Since the template doesn't change for each requets, we can calculate the replacement position and use thoses
-- static position instead. We'll need to do the search/replace in reverse, starting by the last pattern found on the 
-- challenge template.    
local function getPositions()
    local positions = {}
    local item = {"#hashValue", "#requestId"}
    for i=1, #item do
        positions[i] = {
            item[i], 
            string.find(challengeTemplate, item[i], 1)
        }
    end
    table.sort(positions, function(a,b)
        return a[2] > b[2]
    end)
    return positions
end

local positions = getPositions()

-- Fast search/replace of pattern using the position
-- @return string
local function fastReplace(template, start, stop, replacement) 
    return template:sub(1, start - 1) 
        .. replacement 
        .. template:sub(stop + 1, #template)
end

-- Fast search/replace to get the template
local function fastTemplate(replacementValues)
    local response = challengeTemplate
    for i =1, #positions do 
        response = fastReplace(
            response, 
            positions[i][2], 
            positions[i][3], 
            replacementValues[positions[i][1]]
        )
    end
    return response
end


-- Generate a hash with the client IP address, UA, challenge type and a secret, to avoid tampering of the challenge send. 
-- For instance, to avoid a smartass to ask the JS challenge instead of captcha
-- @return md5 hash
function _M.getAntiTamperingHash(ip, ua)
    return md5('ANTI-TAMPERING' .. ip .. (ua or '-') .. 'js' .. o2config.jsSecret .. today())
end

-- Send the HTML code for the JS Challenge 
-- @antiTamperingHash the hash from getAntiTamperingHash
function _M.sendChallengeHtmlPage(antiTamperingHash, requestId) 
    ngx.status = 503
    ngx.header.content_type = "text/html; charset=UTF-8"
    ngx.header.cache_control = "private, max-age=0, no-store, no-cache, must-revalidate, post-check=0, pre-check=0"
    ngx.header.expires = "Thu, 01 Jan 1970 00:00:01 GMT"
    ngx.header.referer_policy = "same-origin"
    ngx.header.tiger_protect_security = "https://faq.o2switch.fr/hebergement-mutualise/tutoriels-cpanel/tiger-protect"
    ngx.say(fastTemplate({
        ['#hashValue'] = antiTamperingHash,
        ['#requestId'] = requestId or '-',
    }))
    ngx.exit(200)
end

-- Return a random number between the cost limit configured
-- @return {numberToGuess, costUpperLimit}
function _M.getRandomNumber()
    math.randomseed(math.random(1,999999999) + time())
    return tostring(math.random(o2config.minCost, o2config.maxCost))
end

-- Return a collection of hashing methods to use
-- @return {'first hash to use', 'second hash to use', '...'}
function _M.getHashingMethod()
    math.randomseed(math.random(1,999999999) + time())
    local hashingMethods = {'md5', 'sha256', 'sha1'}
    local returnValue = {}
    local iteration = math.random(1,4)
    for i=1, iteration do
        table.insert(returnValue, hashingMethods[math.random(#hashingMethods)])
    end
    return returnValue
end

-- Return the hash to guess
-- @return final hash or nil in case of error
function _M.getHash(numberToGuess, hashMethodCollection)
    if type(numberToGuess) ~= 'number' then
        numberToGuess = tostring(numberToGuess)
    end

    local returnValue = numberToGuess
    for k, hashMethod in pairs(hashMethodCollection) do
        local hash, err
        if hashMethod == 'md5' then
            hash, err = hashWrapper.md5(returnValue)
        elseif hashMethod == 'sha1' then
            hash, err = hashWrapper.sha1(returnValue)
        elseif hashMethod == 'sha256' then
            hash, err = hashWrapper.sha256(returnValue)
        else
            o2debug.debugErr('Unsupported hashing method')
            return nil
        end

        if not hash or err ~= nil then
            o2debug.debugErr('Hashing error')
            return nil
        end
        returnValue = hash
    end

    return returnValue
end

-- Return a string that will be used as a challenge ID
-- The challenge ID is used to be extra safe in the case were there's a lot of people behind a public IP address
-- @return string
function _M.generateChallengeId(nginxUniqueId)
    if type(nginxUniqueId) == 'string' then
        return string.sub(nginxUniqueId, 1, 6)
    end

    math.randomseed(math.random(1,999999999) + time())
    return tostring(math.random(100000,999999))
end

-- Persist the challenge details to Redis. It will be used to check the response / solution of the challenge later.
-- @domain The domain name for which the security was triggered
-- @ip The remote_addr
-- @numberToGuess The number to guess (the solution)
-- @return boolean, nil|error
function _M.persist(domain, ip, numberToGuess, challengeId)
    if(type(domain) ~= 'string') then
        o2debug.debugErr("The domain is not valid, not a string.")
        return false, "The domain is not valid, not a string."
    end

    if(type(ip) ~= 'string') then
        o2debug.debugErr("The domain IP address is not valid, not a string")
        return false, "The domain IP address is not valid, not a string"
    end

    if(type(challengeId) ~= 'string') then
        o2debug.debugErr("The challengeId is not valid, not a string : " .. type(challengeId))
        return false, "The challengeId is not valid, not a string"
    end

    if(type(numberToGuess) == 'number') then
        numberToGuess = tostring(numberToGuess)
    end

    if(type(numberToGuess) ~= 'string') then
        o2debug.debugErr("The domain numberToGuess is not valid, not a string, neither a number")
        return false, "The domain hashMethodCollection is not valid, not a string, neither a number"
    end

    local ok, json = pcall(json.encode, {
        ["i"] = ip,
        ["n"] = numberToGuess,
    })
    if not ok or type(json) ~= 'string' then
        o2debug.debugErr("Json encode error")
        return false, "Json encode error"
    end

    local ok, err = o2redis.hset(domain .. ONGOING_CHALLENGE_SUFFIX, challengeId, json, CHALLENGE_REDIS_TTL)
    if not ok or err ~= nil then
        o2debug.debugErr("Persisting error : " .. err)
        return false, "Persisting error : " .. err
    end

    return true, nil
end

-- Retrieve the data of a previously send JS Challenge
-- @return table|nil, errMsg|nil
function _M.retrieve(domain, challengeId)
    if(type(domain) ~= 'string') then
        o2debug.debugErr("The domain is not valid, not a string.")
        return false, "The domain is not valid, not a string."
    end

    if type(challengeId) == 'number' then
        challengeId = tostring(challengeId)
    end

    if(type(challengeId) ~= 'string') then
        o2debug.debugErr("The challengeId is not valid, not a string")
        return false, "The challengeId is not valid, not a string"
    end

    local data, err = o2redis.hget(domain .. ONGOING_CHALLENGE_SUFFIX, challengeId)
    if err ~= nil then
        o2debug.debugErr("Error while retrieving data with hget : " .. err)
        return false, "Persisting error : " .. err
    end

    if type(data) ~= 'string' then
        return nil, nil
    end

    local ok, decoded = pcall(json.decode, data)
    if not ok then
        o2debug.debugErr("Json decode error")
        return false, "Json decode error"
    end

    -- TODO : See to remove the hash collection
    if type(decoded) ~= 'table' or decoded['n'] == nil or decoded['i'] == nil then
        return nil, nil
    end

    return decoded, nil
end

-- Return a challenge JS payload. The JS code will be obfuscated.
-- @hash The hash to guess
-- @hashMethodCollection The collection of hash function name to use
-- @challengeId The challenge ID. It's the equivalent of a session, it's used to store the result in the backend.
function _M.getChallengeJsPayload(hash, hashMethodCollection, challengeId)
    return o2obfuscate.getChallengeJsPayload(hash, hashMethodCollection, challengeId)
end

-- Generate and return the value of the bypass cookie for the HS challenge.
-- When this cookie is passed by the browser, the challenge is not triggered anymore.
-- @clientip
-- @ua 
-- @domain 
-- @return string
function _M.getBypassCookieValue(clientip, ua, domain)
    return md5('JS-CHL-BYPASS' .. clientip .. (ua or '-') .. domain .. salt .. today())
end

function _M.sendSuccessRedirectResponse(url, domain, cookieName, bypassCookieValue)
    ngx.status = 302 -- TODO: try with 307 one day to see if we are able to keep the POST content
    ngx.header.content_type = "text/html; charset=UTF-8"
    ngx.header.cache_control = "private, max-age=0, no-store, no-cache, must-revalidate, post-check=0, pre-check=0"
    ngx.header.expires = "Thu, 01 Jan 1970 00:00:01 GMT"
    ngx.header.referer_policy = "same-origin"
    ngx.header.set_cookie = cookieName .. '=' .. bypassCookieValue .. '; domain=.'.. domain ..'; expires='.. cookieTime(time() + 86400) ..'; path=/; SameSite=Lax; HttpOnly'
    ngx.header.tiger_protect_security = "https://faq.o2switch.fr/hebergement-mutualise/tutoriels-cpanel/tiger-protect"
    ngx.header.location = url
    ngx.say(((gsub([=[
        <HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
        <TITLE>301 Moved</TITLE></HEAD><BODY>
        <H1>301 Moved</H1>
        The document has moved
        <A HREF="#url">here</A>.
        </BODY></HTML>
    ]=], '#url', url))))
    ngx.exit(ngx.HTTP_OK)
end

return _M

Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists