├── README.md ├── botnet_challenge.lua ├── botnet_verify_otp.lua └── haproxy.cfg /README.md: -------------------------------------------------------------------------------- 1 | # haproxy-botnetchallenge 2 | 3 | Implement a js challenge like haproxy enterprise antibonet. 4 | 5 | It's using a totp token. 6 | (currently the values are hardcoded in lua files, i need to export them to external variables) 7 | 8 | The js challenge itself is pretty basic, I'm doing a simply ror13 to avoid simply scrapping of html response. 9 | (you can also obsfucate the js if you want) 10 | 11 | Ideas are welcome, if somebody have idea to implement a nice dynamic challenge js, like haproxy enterprise antibot ! 12 | 13 | 14 | 15 | require lua package: 16 | 17 | https://github.com/tilkinsc/LuaOTP/ 18 | 19 | https://github.com/aiq/basexx/blob/master/lib/basexx.lua 20 | 21 | https://github.com/kikito/sha1.lua 22 | 23 | https://github.com/kennyledet/Algorithm-Implementations/blob/master/ROT13_Cipher/Lua/Yonaba/rot13.lua 24 | -------------------------------------------------------------------------------- /botnet_challenge.lua: -------------------------------------------------------------------------------- 1 | local OTP = require("otp") 2 | local TOTP = require("totp") 3 | local basexx = require("basexx") 4 | 5 | local function botnet_challenge(applet) 6 | 7 | local clientip = applet.f:src() 8 | local useragent = applet.headers["user-agent"][0] 9 | if useragent ~= nil then useragent = "sdfocOSdfjiojicvisdinvc" end 10 | 11 | local randomkey = "dxcKfdojsdfxvcOsdfvc" 12 | local INTERVAL = 15; 13 | local DIGITS = 10; 14 | local BASE32_SECRET = basexx.to_base32( clientip..randomkey..useragent ) 15 | local DIGEST = "SHA1"; 16 | 17 | OTP.type = "totp" 18 | local tdata = OTP.new(BASE32_SECRET, DIGITS, DIGEST, 30) 19 | local token = TOTP.now(tdata) 20 | 21 | 22 | local htmlbegin = [[ 23 | 24 | 25 | Waiting for the redirectiron... 26 | 61 | 148 | 149 | 150 | 151 | 152 | 178 | 179 | 180 |
153 |
154 | 157 | 176 |
177 |
181 | ]] 182 | 183 | local javascriptvar = string.format([[ var n= "%s"; ]],token) 184 | 185 | response = htmlbegin .. javascriptvar .. javascript .. htmlend 186 | 187 | applet:set_status(200) 188 | applet:add_header("content-length", string.len(response)) 189 | applet:add_header("content-type", "text/html") 190 | applet:add_header("Cache-Control", "no-cache, no-store, must-revalidate") 191 | applet:add_header("Pragma", "no-cache") 192 | applet:add_header("Expires", "0") 193 | 194 | applet:start_response() 195 | applet:send(response) 196 | end 197 | core.register_service("botnet_challenge", "http", botnet_challenge) 198 | 199 | 200 | -------------------------------------------------------------------------------- /botnet_verify_otp.lua: -------------------------------------------------------------------------------- 1 | local OTP = require 'otp' 2 | local TOTP = require 'totp' 3 | local basexx = require 'basexx' 4 | local rot13 = require 'rot13' 5 | 6 | core.register_fetches("botnet_verify_otp", function(txn, cookie, alreadyauth, useragent) 7 | local cookie = txn:get_var(cookie) 8 | local alreadyauth = txn:get_var(alreadyauth) 9 | local useragent = txn:get_var(useragent) 10 | local clientip = txn.f:src() 11 | 12 | if alreadyauth ~= nil and alreadyauth == 1 then return true end 13 | if cookie == nil then return false end 14 | 15 | if useragent ~= nil then useragent = "sdfocOSdfjiojicvisdinvc" end 16 | 17 | local status, cookiedecode = pcall(rot13.decipher,cookie); 18 | if status == false then return false end 19 | 20 | local randomkey = "dxcKfdojsdfxvcOsdfvc" 21 | local INTERVAL = 15; 22 | local DIGITS = 10; 23 | local BASE32_SECRET = basexx.to_base32( clientip..randomkey..useragent ) 24 | local DIGEST = "SHA1"; 25 | 26 | OTP.type = "totp" 27 | local tdata = OTP.new(BASE32_SECRET, DIGITS, DIGEST, 30) 28 | local tv1 = TOTP.verify(tdata, cookiedecode, os.time(), 0) 29 | return tv1 30 | end) 31 | -------------------------------------------------------------------------------- /haproxy.cfg: -------------------------------------------------------------------------------- 1 | global 2 | maxconn 20000 3 | log 127.0.0.1 local0 4 | user haproxy 5 | chroot /usr/share/haproxy 6 | pidfile /run/haproxy.pid 7 | stats socket /var/run/haproxy.stat 8 | 9 | lua-load /etc/haproxy/botnet_challenge.lua 10 | lua-load /etc/haproxy/botnet_verify_otp.lua 11 | daemon 12 | 13 | 14 | frontend main 15 | bind :80 16 | mode http 17 | 18 | acl staticfiles path_end -f /etc/haproxy/map.d/staticfiles.map 19 | acl ip_greylist src,map_ip(/etc/haproxy/map.d/greylist.map) -m found 20 | 21 | stick-table type ip size 1m expire 24h store gpc0,gpc1,gpt0,http_req_rate(10s) 22 | #track all request but not assets 23 | http-request track-sc0 src table main if !staticfiles 24 | #track ip-useragent only if cookie MyKey is sent 25 | http-request set-header X-Concat %[src]_%[req.fhdr(User-Agent)] if { req.cook(MyKey) -m found } 26 | http-request track-sc1 req.fhdr(X-Concat) table authcookie if { req.cook(MyKey) -m found } 27 | 28 | #verify otp 29 | http-request set-var(req.authcookie) req.cook(MyKey) 30 | http-request set-var(req.alreadyauth) sc1_get_gpt0 31 | http-request set-var(req.useragent) req.fhdr(user-agent) 32 | acl authok lua.botnet_verify_otp(req.authcookie,req.alreadyauth,req.useragent) -m bool 33 | 34 | #flag gpt=1 in authcookie table with ip/useragent as key if auth is ok 35 | http-request sc-set-gpt0(1) 1 if authok 36 | 37 | #increase gpc1 for each challenge display for greylist 38 | http-request sc-inc-gpc1(0) if !authok ip_greylist 39 | 40 | #display challenge for 10 first requests 41 | http-request use-service lua.botnet_challenge if !authok ip_greylist { src_get_gpc1(FO) lt 10 } 42 | 43 | # tarpit if detected more than 10 challenge display 44 | use_backend tarpit if { src_get_gpc1(main) gt 10 } 45 | 46 | # tarpit if waf have detected more than 10 attack, even if authok 47 | use_backend tarpit if { src_get_gpc0(main) gt 10 } 48 | 49 | 50 | default_backend app 51 | 52 | backend authcookie 53 | stick-table type string len 250 size 1m expire 12h store gpt0 54 | 55 | 56 | backend app 57 | mode http 58 | balance roundrobin 59 | timeout connect 5s 60 | timeout server 30s 61 | timeout queue 30s 62 | 63 | 64 | backend honeypot 65 | mode http 66 | http-request tarpit 67 | 68 | --------------------------------------------------------------------------------