Skip to content

Commit

Permalink
migrated from JA3 to JA3N fingerprint
Browse files Browse the repository at this point in the history
  • Loading branch information
ansibleguy committed May 26, 2024
1 parent 0ff9bcf commit 8fd0634
Show file tree
Hide file tree
Showing 9 changed files with 104 additions and 36 deletions.
5 changes: 3 additions & 2 deletions ExampleWAF.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ There are still some basic WAF features to be implemented.

NOTE: The feature-set this role provides does not come lose to the one [available in HAProxy Enterprise by default](https://www.haproxy.com/solutions/web-application-firewall).

[Fingerprinting Docs](https://github.com/ansibleguy/infra_haproxy/blob/latest/Fingerprinting.md) for detailed information on how you might want to track clients.

## Config

```yaml
Expand Down Expand Up @@ -138,8 +140,7 @@ root@test-ag-haproxy-waf:/# cat /etc/haproxy/conf.d/frontend.cfg
> http-response set-header X-Permitted-Cross-Domain-Policies "none"
> http-response set-header X-XSS-Protection "1; mode=block"
> # SSL fingerprint
> http-request set-header X-FINGERPRINT-JA3-RAW %[ssl_fc_protocol_hello_id],%[ssl_fc_cipherlist_bin(1),be2dec(-,2)],%[ssl_fc_extlist_bin(1),be2dec(-,2)],%[ssl_fc_eclist_bin(1),be2dec(-,2)],%[ssl_fc_ecformats_bin,be2dec(-,1)]
> http-request set-var(txn.fingerprint_ssl) req.fhdr(X-FINGERPRINT-JA3-RAW),digest(md5),hex,lower
> http-request lua.fingerprint_ja3n
> http-request capture var(txn.fingerprint_ssl) len 32
>
> http-request capture req.fhdr(User-Agent) len 200
Expand Down
19 changes: 16 additions & 3 deletions Fingerprinting.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,28 @@ If you want to have a fingerprint that is unique for each client that connects,

Check out my [WAF Docs](https://wiki.superstes.eu/en/latest/1/infra/waf.html) for more details.

## SSL Fingerprint (JA3)
## SSL Fingerprint (JA3N)

This fingerprint will be the same for every HTTP client. Per example: Chrome 118.1.1 will have the same one - no matter were it comes from. This can be pretty useful to track/recognize a distributed attack.

You may not want to use this kind of fingerprint for blocking clients. But it can be combined with other data to limit the block-scope.

If you enable `security.fingerprint_ssl` you can reference it using the variables:

* `var(txn.fingerprint_ssl)` => MD5 hash of JA3 fingerprint
* `var(txn.fingerprint_ssl_raw)` => raw JA3 fingerprint
* `var(txn.fingerprint_ssl)` => MD5 hash of JA3n fingerprint
* `var(txn.fingerprint_ssl_raw)` => raw JA3n fingerprint

To use this kind of fingerprint, you have to enable the `[SSL capture-buffer](https://www.haproxy.com/documentation/haproxy-configuration-manual/latest/#3.2-tune.ssl.capture-buffer-size)`. You may want to set it in the globals via `tune.ssl.capture-buffer-size 96`

### JA3N Notes

The basic JA3 fingerprint is currently not useful as browsers started to randomize the order of their SSL extensions.

JA3N tackles this by sorting these. See also: [tlsfingerprint.io](https://tlsfingerprint.io/norm_fp)

HAProxy Lua script that implements JA3N: [gist.github.com/superstes](https://gist.github.com/superstes/0d0a94cb70f2e2713f4a90fa88160795)

**Examples**:

* Before (JA3): `771,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,0-23-65281-10-11-16-5-34-51-43-13-45-28-65037-41,29-23-24-25-256-257,0`
* After (JA3N): `771,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,0-10-11-13-16-23-28-34-41-43-45-5-51-65037-65281,29-23-24-25-256-257,0`
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,9 +334,9 @@ ansible-vault encrypt_string

Examples:

* Lower rate-limit for bots: `http-request deny deny_status 429 if { var(txn.bot) -m int 1 } { sc_http_req_rate(0) gt 50 }`
* Lower rate-limit for bots: `http-request deny deny_status 429 if !{ var(txn.bot) -m int 0 } { sc_http_req_rate(0) gt 50 }`

* Hard deny bots to register accounts: `http-request deny deny_status 400 if { var(txn.bot) -m int 1 } { method POST } { path_sub -m str -i /register/ }`
* Hard deny bots to register accounts: `http-request deny deny_status 400 if !{ var(txn.bot) -m int 0 } { method POST } { path_sub -m str -i /register/ }`

* Pass the flag to your application to show a pretty error: `http-request add-header X-Bot %[var(txn.bot)]`

Expand Down
4 changes: 3 additions & 1 deletion defaults/main/2_waf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ defaults_waf:
- 'data'
- 'agent'
- 'inspect'
- 'github-camo'

script_kiddy:
excludes: [] # user-defined excludes
Expand Down Expand Up @@ -194,13 +195,14 @@ defaults_waf:
- '/wordpress'

path_end:
- 'login'
- 'login.jsp'
- 'logon.htm'
- 'logon.html'
- 'logincheck'
# scripts etc
- '.php'
- '.php7'
- '.php8'
- '.asp'
- '.aspx'
- '.esp'
Expand Down
8 changes: 8 additions & 0 deletions tasks/debian/install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,11 @@
ansible.builtin.systemd:
name: 'haproxy.service'
enabled: true

- name: HAProxy | Install | Add LUA SSL-Fingerprint module
ansible.builtin.template:
src: "templates{{ HAPROXY_HC.path.lua }}/ja3n.lua.j2"
dest: "{{ HAPROXY_HC.path.lua }}/ja3n.lua"
owner: 'root'
group: 'haproxy'
mode: 0750
50 changes: 25 additions & 25 deletions templates/etc/haproxy/conf.d/inc/security.j2
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,6 @@
http-request deny status 405 default-errorfiles if { method TRACE CONNECT }
{% endif %}

{% if cnf.security.block_script_bots | bool %}
# block well-known script-bots
{% if HAPROXY_WAF.user_agents.script.full | length > 0 %}
http-request deny status {{ HAPROXY_WAF.block_code }} {{ BLOCK_ERRORFILE }} if { req.fhdr(User-Agent) -m str -i -f {{ HAPROXY_HC.path.lst }}/{{ HAPROXY_HC.file.lst.bad_bot_full }} }
{% endif %}
{% if HAPROXY_WAF.user_agents.script.sub | length > 0 %}
http-request deny status {{ HAPROXY_WAF.block_code }} {{ BLOCK_ERRORFILE }} if { req.fhdr(User-Agent) -m sub -i -f {{ HAPROXY_HC.path.lst }}/{{ HAPROXY_HC.file.lst.bad_bot_sub }} }
{% endif %}
{% endif %}
{% if cnf.security.block_bad_crawler_bots | bool %}
# block well-known bad-crawler-bots
{% if HAPROXY_WAF.user_agents.bad_crawlers.full | length > 0 %}
http-request deny status {{ HAPROXY_WAF.block_code }} {{ BLOCK_ERRORFILE }} if { req.fhdr(User-Agent) -m str -i -f {{ HAPROXY_HC.path.lst }}/{{ HAPROXY_HC.file.lst.crawler_full }} }
{% endif %}
{% if HAPROXY_WAF.user_agents.bad_crawlers.sub | length > 0 %}
http-request deny status {{ HAPROXY_WAF.block_code }} {{ BLOCK_ERRORFILE }} if { req.fhdr(User-Agent) -m sub -i -f {{ HAPROXY_HC.path.lst }}/{{ HAPROXY_HC.file.lst.crawler_sub }} }
{% endif %}
{% endif %}
{% if cnf.security.block_script_kiddies | bool %}
# block script-kiddy requests
http-request deny status {{ HAPROXY_WAF.block_code }} {{ BLOCK_ERRORFILE }} if { path -m beg -i -f {{ HAPROXY_HC.path.lst }}/{{ HAPROXY_HC.file.lst.script_kiddy_beg }} }
http-request deny status {{ HAPROXY_WAF.block_code }} {{ BLOCK_ERRORFILE }} if { path -m end -i -f {{ HAPROXY_HC.path.lst }}/{{ HAPROXY_HC.file.lst.script_kiddy_end }} }
http-request deny status {{ HAPROXY_WAF.block_code }} {{ BLOCK_ERRORFILE }} if { path -m sub -i -f {{ HAPROXY_HC.path.lst }}/{{ HAPROXY_HC.file.lst.script_kiddy_sub }} }
{% endif %}
{% if cnf.security.flag_bots | bool %}
# FLAG BOTS
## flag bots by common user-agent substrings
Expand Down Expand Up @@ -65,4 +41,28 @@
http-request set-var(txn.bot) int(0) if !{ var(txn.bot) -m found }
http-request capture var(txn.bot) len 1

{% endif %}
{% endif %}
{% if cnf.security.block_script_bots | bool %}
# block well-known script-bots
{% if HAPROXY_WAF.user_agents.script.full | length > 0 %}
http-request deny status {{ HAPROXY_WAF.block_code }} {{ BLOCK_ERRORFILE }} if { req.fhdr(User-Agent) -m str -i -f {{ HAPROXY_HC.path.lst }}/{{ HAPROXY_HC.file.lst.bad_bot_full }} }
{% endif %}
{% if HAPROXY_WAF.user_agents.script.sub | length > 0 %}
http-request deny status {{ HAPROXY_WAF.block_code }} {{ BLOCK_ERRORFILE }} if { req.fhdr(User-Agent) -m sub -i -f {{ HAPROXY_HC.path.lst }}/{{ HAPROXY_HC.file.lst.bad_bot_sub }} }
{% endif %}
{% endif %}
{% if cnf.security.block_bad_crawler_bots | bool %}
# block well-known bad-crawler-bots
{% if HAPROXY_WAF.user_agents.bad_crawlers.full | length > 0 %}
http-request deny status {{ HAPROXY_WAF.block_code }} {{ BLOCK_ERRORFILE }} if { req.fhdr(User-Agent) -m str -i -f {{ HAPROXY_HC.path.lst }}/{{ HAPROXY_HC.file.lst.crawler_full }} }
{% endif %}
{% if HAPROXY_WAF.user_agents.bad_crawlers.sub | length > 0 %}
http-request deny status {{ HAPROXY_WAF.block_code }} {{ BLOCK_ERRORFILE }} if { req.fhdr(User-Agent) -m sub -i -f {{ HAPROXY_HC.path.lst }}/{{ HAPROXY_HC.file.lst.crawler_sub }} }
{% endif %}
{% endif %}
{% if cnf.security.block_script_kiddies | bool %}
# block script-kiddy requests
http-request deny status {{ HAPROXY_WAF.block_code }} {{ BLOCK_ERRORFILE }} if { path -m beg -i -f {{ HAPROXY_HC.path.lst }}/{{ HAPROXY_HC.file.lst.script_kiddy_beg }} }
http-request deny status {{ HAPROXY_WAF.block_code }} {{ BLOCK_ERRORFILE }} if { path -m end -i -f {{ HAPROXY_HC.path.lst }}/{{ HAPROXY_HC.file.lst.script_kiddy_end }} }
http-request deny status {{ HAPROXY_WAF.block_code }} {{ BLOCK_ERRORFILE }} if { path -m sub -i -f {{ HAPROXY_HC.path.lst }}/{{ HAPROXY_HC.file.lst.script_kiddy_sub }} }
{% endif %}
3 changes: 1 addition & 2 deletions templates/etc/haproxy/conf.d/inc/security_only_fe.j2
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
{% endif %}
{% if cnf.security.fingerprint_ssl | bool %}
# SSL fingerprint
http-request set-header X-FINGERPRINT-JA3-RAW %[ssl_fc_protocol_hello_id],%[ssl_fc_cipherlist_bin(1),be2dec(-,2)],%[ssl_fc_extlist_bin(1),be2dec(-,2)],%[ssl_fc_eclist_bin(1),be2dec(-,2)],%[ssl_fc_ecformats_bin,be2dec(-,1)]
http-request set-var(txn.fingerprint_ssl) req.fhdr(X-FINGERPRINT-JA3-RAW),digest(md5),hex,lower
http-request lua.fingerprint_ja3n
http-request capture var(txn.fingerprint_ssl) len 32
{% endif %}
5 changes: 4 additions & 1 deletion templates/etc/haproxy/haproxy.cfg.j2
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ global
{% if HAPROXY_CONFIG.geoip.enable | bool %}
lua-load {{ HAPROXY_HC.path.lua }}/geoip.lua
{% endif %}
{% if HAPROXY_CONFIG.frontends | ssl_fingerprint_active and 'tune.ssl.capture-buffer-size' not in HAPROXY_CONFIG.global %}
{% if HAPROXY_CONFIG.frontends | ssl_fingerprint_active %}
lua-load {{ HAPROXY_HC.path.lua }}/ja3n.lua
{% if 'tune.ssl.capture-buffer-size' not in HAPROXY_CONFIG.global %}
tune.ssl.capture-buffer-size 96
{% endif %}
{% endif %}

{% for key, value in HAPROXY_CONFIG.global.items() %}
Expand Down
42 changes: 42 additions & 0 deletions templates/etc/haproxy/lua/ja3n.lua.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
-- {{ ansible_managed }}
-- ansibleguy.infra_haproxy
-- source: https://gist.github.com/superstes/0d0a94cb70f2e2713f4a90fa88160795
-- see: https://tlsfingerprint.io/norm_fp
-- JA3N = sorted extensions to tackle browsers randomizing their order
-- examples:
-- before (JA3): 771,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,0-23-65281-10-11-16-5-34-51-43-13-45-28-65037-41,29-23-24-25-256-257,0
-- after (JA3N): 771,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,0-10-11-13-16-23-28-34-41-43-45-5-51-65037-65281,29-23-24-25-256-257,0

function split_string(str, delimiter)
local result = {}
local from = 1
local delim_from, delim_to = string.find(str, delimiter, from)
while delim_from do
table.insert(result, string.sub(str, from , delim_from-1))
from = delim_to + 1
delim_from, delim_to = string.find(str, delimiter, from)
end
table.insert(result, string.sub(str, from))
return result
end

function fingerprint_ja3n(txn)
local p1 = tostring(txn.f:ssl_fc_protocol_hello_id())
local p2 = tostring(txn.c:be2dec(txn.f:ssl_fc_cipherlist_bin(1),"-",2))

local p3u = tostring(txn.c:be2dec(txn.f:ssl_fc_extlist_bin(1),"-",2))
local p3l = split_string(p3u, "-")
table.sort(p3l)
local p3 = table.concat(p3l, "-")

local p4 = tostring(txn.c:be2dec(txn.f:ssl_fc_eclist_bin(1),"-",2))
local p5 = tostring(txn.c:be2dec(txn.f:ssl_fc_ecformats_bin(),"-",1))

local fingerprint = p1 .. "," .. p2 .. "," .. p3 .. "," .. p4 .. "," .. p5
local fingerprint_hash = string.lower(tostring(txn.c:hex(txn.c:digest(fingerprint, "md5"))))

txn:set_var('txn.fingerprint_ssl_raw', fingerprint)
txn:set_var('txn.fingerprint_ssl', fingerprint_hash)
end

core.register_action('fingerprint_ja3n', {'tcp-req', 'http-req'}, fingerprint_ja3n)

0 comments on commit 8fd0634

Please sign in to comment.