Challenge
No file, no binary — just a URL:
https://broncosec.com/BroncoCTF
The flag format is given as bronco{XXXX...}. Nothing else. This is an
OSINT / web-recon style challenge: the flag is broken into 8 numbered
fragments, scattered across the live site using a mix of static HTML,
downloadable files, and client-side JavaScript behavior. The goal is to
find all 8 pieces and assemble them in order.
Recon
Fetching the page's rendered HTML directly gives a first look. Most of
the page is a fairly standard "CTF landing page" — hero banner, stats
counters, sponsor logo, footer credits. Scanning through it for anything
that looks out of place (odd alt text, title attributes, query
strings, filenames) turns up four fragments immediately, sitting in
plain sight in the static markup:
| # | Fragment | Location |
|---|---|---|
| 1 | bronco{h |
Plain text at the very end of the page footer, after the credits |
| 3 | 0und_th3 |
title attribute on the "Join the Competition" button/link |
| 6 | ut31y_n0 |
Query string on the "BroncoCTF 2026...?" repository card: href="/BroncoCTF?KEY=6-ut31y_n0"
|
| 8 | _4t_411} |
alt text on the sixth stats card (the one using correct-flag-colorized.svg) |
That's 4 of 8, found just by reading the rendered HTML carefully.
Piece 7 — a hidden download
The "About the Competition" text includes:
BroncoCTF 2026 will be our fifth CTF...
The word "2026" is a hyperlink to /7.txt, disguised as normal body
text (no obvious styling gives it away as a link in the rendered page).
Fetching it directly:
$ curl https://broncosec.com/7.txt
7 - _w0rr135
Piece 7 found.
Pieces 2, 4, 5 — hidden in client-side JavaScript
The remaining three fragments never appear in the static HTML at all —
they're generated at runtime by JavaScript event handlers, meaning a
plain curl/fetch of the page will never reveal them. The fix is to
pull down the site's actual JS bundles and grep them directly.
Step 1 — enumerate the script chunks. Looking at the <script src=...>
tags in the raw page source (Next.js app, so chunks are hashed filenames
under /_next/static/chunks/):
$ curl -sO https://broncosec.com/_next/static/chunks/e785679bf8074938.js
$ curl -sO https://broncosec.com/_next/static/chunks/f31cf569852813cb.js
... (and the rest of the referenced chunks)
Step 2 — grep for anything flag-shaped:
$ grep -rn "bronco{" .
$ grep -rnE "[0-9]+ - " .
This immediately surfaces three onClick handlers and one DOM-injection
trick buried in the minified React component source:
Fragment 2 — hidden behind a click handler on the word "flags":
onClick: () => {
let e = document.getElementById("addtext");
e && e.firstChild && (e.firstChild.textContent += "2 - 3y_y0u_f");
new Audio("ding.oga").play();
}
Clicking the word "flags" in the body text appends 2 - 3y_y0u_f
into a hidden <div id="addtext"> on the page and plays a sound. A
static HTML fetch will never show this — it only exists after the click
event fires and mutates the DOM.
Fragment 4 — hidden behind clicking the 🙋 emoji:
onClick: () => {
document.cookie = "KEY4=4 - m_4ll_w1; path=/";
...
new Audio("puzzle.oga").play();
}
Clicking the raised-hand emoji under "Online Bragging Rights" sets a
cookie named KEY4 containing the fragment, and appends a 🍪 emoji
next to it as visual confirmation.
Fragment 5 — hidden as a dynamically-injected HTML comment:
function a({comment: e}) {
let n = useRef(null);
useEffect(() => {
n.current && (n.current.outerHTML = `<!-- ${e} -->`);
}, [e]);
return <script ref={n} type="text/placeholder" />;
}
...
<a comment="!!! 5 - th_4b501 !!!" />
A placeholder <script> tag gets replaced, after mount, with an HTML
comment containing the fragment. This only exists in the live DOM
after React hydrates — invisible both to a static curl and to
"view source", since browsers' "view source" shows the original
server-rendered HTML, not the post-hydration DOM. Only "Inspect
Element" (which reflects the live DOM) or a JS-aware fetch would catch
it.
Assembling the flag
Ordering all 8 recovered fragments by their embedded index:
1: bronco{h
2: 3y_y0u_f
3: 0und_th3
4: m_4ll_w1
5: th_4b501
6: ut31y_n0
7: _w0rr135
8: _4t_411}
Concatenated:
bronco{h3y_y0u_f0und_th3m_4ll_w1th_4b501ut31y_n0_w0rr135_4t_411}
Leetspeak-decoded, this reads:
"hey you found them all with absolutely no worries at all"
— a fitting message for a challenge literally about finding every
scattered fragment.
Flag
bronco{h3y_y0u_f0und_th3m_4ll_w1th_4b501ut31y_n0_w0rr135_4t_411}
Key Takeaways
-
Static fetches only show you half the picture on modern JS-framework
sites. Anything gated behind an
onClick, auseEffect, or other client-side hydration logic is invisible tocurl/basicweb_fetch— you have to either pull and read the actual JS bundles, or interact with a live, JS-executing browser (DevTools/Inspect Element) to see DOM state that only exists after the page runs. -
Downloading and grepping the full JS bundle set (
/_next/static/chunks/*.jsfor Next.js apps) is a fast, reliable way to recover client-side logic and embedded strings without needing to manually trigger every interaction in a browser first. -
Minified React source is still greppable. Even heavily minified
JSX compiles down to recognizable patterns (
onClick:, string literals, JSX attribute names) that survive minification well enough togrepfor target patterns like flag formats or numbered fragments. - Distinguish "view source" vs. "Inspect Element": view-source shows the original server response; Inspect Element (or any DOM-reading tool) shows the current, JS-mutated state. Challenges that hide data in post-hydration DOM changes require the latter.
Top comments (0)