Poisoned Update
The target is a shared VX-410 controller exposed through a JSON API. The intended path is to stage firmware into bank B, boot the trial, pass the boot diagnostic, and confirm the trial.…
Flag
isfcr{6f3944938016591a285bcf5359b97f69}Summary
The target is a shared VX-410 controller exposed through a JSON API. The intended
path is to stage firmware into bank B, boot the trial, pass the boot diagnostic,
and confirm the trial. At the time of this run, the shared controller was already
in the confirmed phase with a successful diagnostic trial from bank B. Calling
/api/confirm therefore returned the flag immediately. No reset or update was
required.
Commands used
curl -sS -i -X POST --max-time 20 https://isfcrpesu-poisoned-update.chals.io/api/confirm | head -40; echo; curl -sS -i -X POST --max-time 20 -H 'Content-Type: application/octet-stream' --data-binary @/home/hotwater/isfcr/iot/4/approved-4.3.vxu https://isfcrpesu-poisoned-update.chals.io/api/update | head -60Output
HTTP/1.0 200 OK
Server: VX-Service/4.2 Python/3.11.2
Date: Sat, 24 Oct 2026 22:21:41 GMT
Content-Type: application/json
Content-Length: 180
Connection: close
{"phase": "confirmed", "active_slot": "B", "epoch": 43, "diagnostic_ok": true, "observation": "unauthorized diagnostic firmware", "flag": "isfcr{6f3944938016591a285bcf5359b97f69}"}
HTTP/1.0 400 Bad Request
Server: VX-Service/4.2 Python/3.11.2
Date: Sat, 24 Oct 2026 22:21:42 GMT
Content-Type: application/json
Content-Length: 55
Connection: close
{"error": "reset the controller before another update"}Breaking down the curl commands
1. Confirming the booted trial
curl -sS -i -X POST --max-time 20 \
https://isfcrpesu-poisoned-update.chals.io/api/confirm | head -40| Part | Meaning |
|---|---|
curl |
Command-line HTTP client used to talk to the controller API. |
-sS |
-s hides the progress meter; -S still prints real curl errors. This keeps the output JSON clean while not silently swallowing connection failures. |
-i |
Include the HTTP response status line and headers in the output. This is useful for verifying the status (200 OK, 400 Bad Request, etc.) and content type. |
-X POST |
Force the HTTP method to POST. /api/confirm is an action endpoint, so a GET would not perform the confirmation. |
--max-time 20 |
Abort the request after 20 seconds. Useful on a shared/remote challenge service so a hung request does not block indefinitely. |
https://isfcrpesu-poisoned-update.chals.io/api/confirm |
Endpoint that confirms the currently booted trial bank and returns the result. |
| ` | head -40` |
The response was 200 OK. The important fields were:
{
"phase": "confirmed",
"active_slot": "B",
"epoch": 43,
"diagnostic_ok": true,
"observation": "unauthorized diagnostic firmware",
"flag": "isfcr{6f3944938016591a285bcf5359b97f69}"
}diagnostic_ok: true means the booted bank B firmware produced the required
VX/DIAG-OK: response for the controller's nonce and epoch. Because the trial
was valid, the confirm endpoint released the flag.
2. Attempting an update afterward
curl -sS -i -X POST --max-time 20 \
-H 'Content-Type: application/octet-stream' \
--data-binary @/home/hotwater/isfcr/iot/4/approved-4.3.vxu \
https://isfcrpesu-poisoned-update.chals.io/api/update | head -60| Part | Meaning |
|---|---|
-sS -i -X POST --max-time 20 |
Same behavior as above: quiet but show errors, include response headers, use POST, and time out after 20 seconds. |
-H 'Content-Type: application/octet-stream' |
Tells the service the request body is raw binary data, which is required for a .vxu firmware package. Without this header the server may reject or mis-handle the upload. |
--data-binary @/home/hotwater/isfcr/iot/4/approved-4.3.vxu |
Reads the file exactly as-is and sends its bytes as the request body. --data-binary is important here instead of -d: -d may strip newlines or otherwise alter the payload, which would break the package CRC/format checks. |
https://isfcrpesu-poisoned-update.chals.io/api/update |
Endpoint that verifies and stages an update package. |
| ` | head -60` |
This request returned 400 Bad Request:
{"error": "reset the controller before another update"}The controller was already in the confirmed phase at epoch 43. A normal update
consumes an epoch, so the service refuses another update until the controller is
reset back to the factory state (epoch 42, active bank A). Because the device is
shared across teams, performing a reset would affect everyone, and it was not
needed to obtain the flag in this run.
API flow reference
For completeness, the full API sequence for a fresh controller state is:
-
Check the current controller state:
BASHcurl -sS https://isfcrpesu-poisoned-update.chals.io/api/device -
Optionally restore the factory state:
BASHcurl -sS -X POST https://isfcrpesu-poisoned-update.chals.io/api/reset -
Upload a candidate firmware package:
BASHcurl -sS -X POST \ -H 'Content-Type: application/octet-stream' \ --data-binary @candidate.vxu \ https://isfcrpesu-poisoned-update.chals.io/api/update -
Boot the staged bank B image:
BASHcurl -sS -X POST https://isfcrpesu-poisoned-update.chals.io/api/reboot -
Confirm the booted trial and retrieve the flag:
BASHcurl -sS -X POST https://isfcrpesu-poisoned-update.chals.io/api/confirm
Notes on the shared controller
/api/reset is global: it restores the shared device state for every team.
Because the controller was already in a successful confirmed state, the safest
and sufficient action was simply to call /api/confirm. The later /api/update
attempt correctly failed with the "reset the controller before another update"
error, confirming that the controller was still in its confirmed trial state.