├── 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 |
153 |
154 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 | This is an automatic process. Your browser will redirect to your requested content in 5 seconds.
175 |
176 |
177 | |
178 |
179 |
180 |
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 |
--------------------------------------------------------------------------------