← all writeups
WebHuntress CTF 2026

Trust the Build

A Go (Fiber) update-manifest activator. You get a signed manifest:

{"package":"local-telemetry-adapter","version":"1.0.13078","artifact":{"build":"release-13078","hook":"passive_health"},"artifact_sha256":"c25168d2...9766","signature":"62aa5d91...f321"}

The instinct is to forge the signature — don't bother, it's a keyed MAC with a server-side secret (I tested every obvious preimage; none match). The real question is what the signature covers. Mutating fields one at a time shows it:

  • change artifact_sha256 / blank it / drop signature → signed_manifest_rejected (so the MAC covers the hash)
  • change package/version → update_not_found (catalog key)
  • change build → artifact_not_supported (artifact selector)
  • change artifact.hook → accepted!

The hook is what the policy maps to routine vs restricted (passive_health = routine, privileged_export = restricted), and it sits outside the signed scope. So you replay the legitimately-signed routine manifest verbatim and just flip the hook:

curl -s https://<host>/inspect-4e00a5 -H 'Content-Type: application/json' \
  -d '{"package":"local-telemetry-adapter","version":"1.0.13078","artifact":{"build":"release-13078","hook":"privileged_export"},"artifact_sha256":"c25168d2...9766","signature":"62aa5d91...f321"}'
{"effect":"unverified_update_activated","ok":true,"evidence":"proof_..."}

The integrity contract binds the signature to the artifact bytes but not to the field that actually drives authorization. Lovely bug.