Security – How to prevent nmap from fingerprinting HAProxy

haproxynmapport-scanningSecurity

In our periodic security scans, our HAProxy instances are always reported as a version disclosure vulnerability. On further inspection, it appears that there are no version banners in any responses and that nmap is responsible for detecting HAProxy based on some sort of fingerprinting.

Our HAProxy instances will autonomously redirect HTTP to HTTPS, while proxying the SSL traffic to the different backend systems. The version disclosure is only reported on the HTTP (80) port, not on the HTTPS (443) port

Here is a sample nmap output illustrating the issue:

$ nmap -sV --script=http-headers example.com

PORT    STATE SERVICE    VERSION
80/tcp  open  http-proxy HAProxy http proxy 1.3.1 or later
| http-headers:
|   Content-length: 0
|   Location: https://example.com/
|   Connection: close
|
|_  (Request type: GET)
443/tcp open  ssl/http   nginx
| http-headers:
|   Server: nginx
|   Content-Type: application/json
|   Transfer-Encoding: chunked
|   Connection: close
|   Cache-Control: no-cache
|   Date: Thu, 06 Sep 2018 14:46:47 GMT
|   X-Frame-Options: SAMEORIGIN
|   X-Xss-Protection: 1; mode=block
|   X-Content-Type-Options: nosniff
|   Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'
|   Strict-Transport-Security: max-age=16000000; includeSubDomains; preload;
|   X-Forwarded-Proto: https
|
|_  (Request type: GET)
|_http-server-header: nginx
Service Info: Device: load balancer

How does nmap exactly fingerprint HAProxy, since there is no banner/header present to reveal its presence? I presume nmap it will look at the specific 301 payload or some TCP fingerprint which is unique to HAProxy.

And the ultimate question, how do I prevent nmap from detecting HAProxy in the first place?

Best Answer

Nmap identifies HAProxy based on what versions added which headers to the response. The match lines are in the nmap-service-probes file in the Nmap source. Here are some selected comments from the file to illustrate how this was accomplished:


# HAProxy responses are mostly from http_err_msgs, HTTP_401_fmt, and HTTP_407_fmt in
# http://git.haproxy.org/?p=haproxy.git;a=blob;f=src/proto_http.c
# Only statuses 200, 403, and 503 are likely to result from from GetRequest;
# other probes can match via fallbacks.
match http-proxy m|^HTTP/1\.0 200 OK\r\nCache-Control: no-cache\r\nConnection: close\r\nContent-Type: text/html\r\n\r\n<html><body><h1>200 OK</h1>\nHAProxy: service ready\.\n</body></html>\n$| p/HAProxy http proxy/ v/before 1.5.0/ d/load balancer/ cpe:/a:haproxy:haproxy/

# Statuses 400, 401, 403, 408, 500, 502, 503, and 504 gained "Content-Type: text/html" in v1.3.1.
# http://git.haproxy.org/?p=haproxy.git;a=commitdiff;h=791d66d3634dde12339d4294aff55a1aed7518e3;hp=b9e98b683612b29ef939c10d3d00be27de26534a
match http-proxy m|^HTTP/1\.0 400 Bad request\r\nCache-Control: no-cache\r\nConnection: close\r\nContent-Type: text/html\r\n\r\n<html><body><h1>400 Bad request</h1>\nYour browser sent an invalid request\.\n</body></html>\n$| p/HAProxy http proxy/ v/1.3.1 or later/ d/load balancer/ cpe:/a:haproxy:haproxy/

# Statuses 400, 401, 403, 408, 500, 502, 503, and 504 gained "Content-Type: text/html" in v1.3.1.
# http://git.haproxy.org/?p=haproxy.git;a=commitdiff;h=791d66d3634dde12339d4294aff55a1aed7518e3;hp=b9e98b683612b29ef939c10d3d00be27de26534a
match http-proxy m|^HTTP/1\.0 400 Bad request\r\nCache-Control: no-cache\r\nConnection: close\r\nContent-Type: text/html\r\n\r\n<html><body><h1>400 Bad request</h1>\nYour browser sent an invalid request\.\n</body></html>\n$| p/HAProxy http proxy/ v/1.3.1 or later/ d/load balancer/ cpe:/a:haproxy:haproxy/

You can't really prevent this without using a reverse proxy of some sort, and even that will most likely be fingerprintable. Find a way to document this as a false positive or accepted risk: there is no way to avoid this sort of thing as it is not configurable.