Skip to main content

THJCC 2026 Summer — Official Writeup

· Updated: Aug 16, 2026 · CTF, THJCC, Forensics, WriteUp

Table of contents

This is my official writeup for the NoNo challenge I made for THJCC 2026 Summer. It is a log-analysis forensics task, and the whole point is to not get baited by the noise.


NoNo

Our SOC pulled the HTTP logs off chal.thjcc.org after an alert fired overnight. Find the secret message within these logs :)

NoNo challenge on CTFd

Difficulty Easy
Flag THJCC{f0ll0w_th3_str34m_2_th3_h1dd3n_r3p0rt}

The files you get

nono-challenge.tar.gz unpacks to four files, three logs and a capture:

File Purpose
nginx-access.ndjson ECS-format nginx access log, the main surface
portal-app.ndjson application-layer log
modsec-waf.ndjson ModSecurity WAF log
capture.pcap 500-packet HTTP capture of the same traffic, for optional confirmation

The logs are ndjson, one JSON object per line, so you can throw them into ELK, use jq, or just open them and read. I go with jq below.

I made the logs deliberately loud. They are stuffed with scary-looking attacker noise and fake leads:

Type Contents
CVE / attack noise Log4Shell, Spring4Shell, Confluence OGNL, SQLi, SSRF to 169.254.169.254, stored XSS, a /api/ping command-injection chain
Fake flag endpoints /flag, /api/v1/flag, /.git/config
Look-alike trap /s3cr3t/report (ordinary o, one character off the real rep0rt)

None of these give you the flag. They are just there to bury the signal and see if you get pulled around by keywords.

Analysis

There is really only one thing that matters. Almost every request in the logs hits the public vhost chal.thjcc.org:50000, and exactly one hits an internal vhost, internal.portal. Count the distinct values of url.domain and that anomaly pops out by itself.

That one internal request is GET /s3cr3t/rep0rt (note the zero in rep0rt), returning HTTP 200. internal.portal is an internal vhost name you cannot reach from outside, but it is the same box as the public server, so you keep the path and just swap the host for the public domain you were given.

Solution

Count url.domain with jq. Everything is chal.thjcc.org:50000 except one hit, internal.portal, which is GET /s3cr3t/rep0rt, HTTP 200, from source 10.0.2.15, with url.full: http://internal.portal/s3cr3t/rep0rt:

$ jq -r '.["url.domain"]' nginx-access.ndjson | sort | uniq -c

# Output
    499 chal.thjcc.org:50000
      1 internal.portal

$ jq -c 'select(.["url.domain"]=="internal.portal")' nginx-access.ndjson

# Output
{"@timestamp":"2025-08-15T03:13:24Z","event.dataset":"nginx.access","source.ip":"10.0.2.15","destination.ip":"172.67.74.226","url.domain":"internal.portal","http.request.method":"GET","url.path":"/s3cr3t/rep0rt","url.original":"/s3cr3t/rep0rt","url.full":"http://internal.portal/s3cr3t/rep0rt","http.response.status_code":200,"http.response.body.bytes":46,"user_agent.original":"Mozilla/5.0","message":"10.0.2.15 - internal.portal \"GET /s3cr3t/rep0rt HTTP/1.1\" 200 46"}

If you want to confirm it against the pcap, Follow HTTP Stream in Wireshark on the request with Host: internal.portal shows the same GET /s3cr3t/rep0rt.

Then just hit that path on the live portal and you get the flag. One small gotcha: without the trailing slash nginx replies 301 Moved Permanently, so remember to add the slash to actually land on the report page:

$ curl http://chal.thjcc.org:50000/s3cr3t/rep0rt/

# Output
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Internal Report</title>...</head><body ...><header ...><span class="font-mono text-sm text-neutral-400">internal.portal</span><span class="ml-auto text-xs text-neutral-600 font-mono">INTERNAL REPORT · CONFIDENTIAL</span></header><main ...><div ...><p class="text-xs text-neutral-600 font-mono mb-1">// internal use only</p><h1 ...>Quarterly Access Report</h1><div ...><p class="text-xs text-neutral-500 mb-2">report token</p><code class="text-emerald-400 break-all">THJCC{f0ll0w_th3_str34m_2_th3_h1dd3n_r3p0rt}</code></div></div></main></body></html>

The report page renders a “Quarterly Access Report”, and that report token is the flag. If you cannot be bothered reading the HTML, one grep grabs it:

$ curl -s http://chal.thjcc.org:50000/s3cr3t/rep0rt/ | grep -o "THJCC{[^}]*}"

# Output
THJCC{f0ll0w_th3_str34m_2_th3_h1dd3n_r3p0rt}

Flag: THJCC{f0ll0w_th3_str34m_2_th3_h1dd3n_r3p0rt}

Source code

Full challenge source is on my repo: UmmItKin/CTFs-chal · THJCC 2026 Summer/NoNo. The flag page is a plain Astro page that only exists at the leaked path, so there was never anything to brute-force, you just had to find the path in the logs:

---
// Hidden flag page — reachable only via the path leaked in capture.pcap.
const FLAG = 'THJCC{f0ll0w_th3_str34m_2_th3_h1dd3n_r3p0rt}'
---

<!-- ... -->
<code class="text-emerald-400 break-all">{FLAG}</code>

The trailing-slash 301 is just the Astro directory build plus nginx try_files:

# Astro 'directory' build: /s3cr3t/rep0rt -> /s3cr3t/rep0rt/index.html
location / {
    try_files $uri $uri/ $uri.html =404;
}

Closing thoughts

This is a pretty simple log-analysis task, which felt about right for THJCC. I was not trying to make it too hard. I just wanted players to practice one thing: volume is not signal. A wall of scary CVE names is easy to chase for an hour, but the anomaly that actually matters is quiet, just that one request to a host that shows up nowhere else :)