$cat uvod-u-web-sigurnost.mdWebHackultet 2026
Uvod u Web Sigurnost (Introduction to Web Security)
A search portal blocks any query or result containing the flag outright, but never blocks a mere substring of it, turning the filter itself into a way to read the flag out one piece at a time.
$man uvod-u-web-sigurnost
What this one teaches:
- Building a boolean oracle out of a filter that only checks for the full secret, not pieces of it.
- Growing a known fragment outward, left and right, instead of searching character by character from a fixed start.
Task description
Can you get past our data leak protection and find out the flag?
The original was in Croatian; this is my translation.
The protections
The target is a small PHP search portal that searches the contents of a few text files and shows snippets of what matched. Searching short, broad terms like a reveals there are at least three files, named 01_ncert.txt, 02_lorem_ipsum.txt, and 99_flag.txt.
Two filters guard the flag:
- Input filter: a query containing the literal text
CTF2026is rejected outright, withZABRANJENI STRING(“forbidden string”) in place of any results. - Output filter: if a search result’s content contains
CTF2026, the snippet is replaced withNEMATE OVLASTI(“you don’t have permission”) instead of being shown.
Both filters check for the complete prefix CTF2026, which never appears alone in a query or a result unless the flag itself does.
The gap in the filter
Neither filter does anything about searching for a shorter piece of the flag’s content, a substring that doesn’t itself contain CTF2026. Searching for part of the flag’s digits still matches inside 99_flag.txt, and the response still says so, through the exact same “you don’t have permission” wording the output filter would otherwise give for any match containing the forbidden prefix. That response, present or absent, is a boolean oracle: for any string, it tells us whether that string appears somewhere in the flag file.
Growing the known fragment
Starting from single digits, each tried one at a time, the oracle tells us which of 0 through 9 appear anywhere in the flag file at all. From there, two-digit, then three-digit substrings narrow things down further, each confirmed hit a small window into the file’s actual content.
Several of these confirmed substrings overlap once there are enough of them. For instance, two independently discovered hits, 201566840 and 18642, both extend (one to the left, one to the right) into a shared middle section, 220, and stitching them together at that overlap gives a single longer confirmed substring, 18642201566840. Repeating that, extending every known fragment one digit at a time in both directions and merging overlaps, eventually produces a string that can’t be extended any further in either direction: 186422015668400.
That’s the flag’s content. Submitting it alongside a sanity check, confirming that the file matches 186422015668400 but not 1864220156684001, verifies there’s nothing more to find in either direction.
Automating the growth
def in_flag(q):
resp = fetch(URL + "?q=" + quote(q))
return "NEMATE OVLASTI" in resp
def extend(s):
changed = True
while changed:
changed = False
for d in "0123456789":
if in_flag(s + d):
s += d; changed = True; break
if in_flag(d + s):
s = d + s; changed = True; break
return s
Seeded with any confirmed short hit (we used 1566), extend() walks outward in both directions until no digit extends it further, which lands directly on the full flag content: CTF2026[186422015668400].
$cat flag.txt reveal
CTF2026[186422015668400]