DEV Community

Cover image for BroncoCTF : Unblur me Writeup
Yogeshwar Peela
Yogeshwar Peela

Posted on • Originally published at exploitnotes.hashnode.dev

BroncoCTF : Unblur me Writeup

Challenge

A web page at https://broncoctf-unblur-me.chals.io/ presents a
"Calculus Review" quiz:

Solve 500 derivatives to unblur the image and unlock a secret.
Progress: 0 / 500
Enter fullscreen mode Exit fullscreen mode

A blurred image sits behind a quiz form. The stated task is to answer
500 randomly-generated derivative problems in a row (get one wrong and
the counter resets to 0) in order to trigger the JavaScript that removes
the CSS blur() filter from the image. Doing that legitimately would be
slow and error-prone — worth checking whether the "500 correct answers"
gate is actually enforced anywhere but the browser.

Recon

Fetching the page source directly:

$ curl https://broncoctf-unblur-me.chals.io/
Enter fullscreen mode Exit fullscreen mode

The interesting logic is all client-side JavaScript:

function checkAnswer() {
  const userAnswer = parseInt(document.getElementById('answer').value);
  if (userAnswer === currentAnswer) {
    correctCount++;
    ...
    if (correctCount >= 500) {
      // reveal image
      flag.style.filter = "none";
      flag.style.pointerEvents = "auto";
    }
  }
}

function loadSecretImage() {
  fetch('/api/v1/internal/fetch-config-blob')
    .then(response => response.blob())
    .then(blob => {
      const blobUrl = URL.createObjectURL(blob);
      document.getElementById('flag-image').src = blobUrl;
    });
}
Enter fullscreen mode Exit fullscreen mode

Two important observations:

  1. The "solve 500 problems" gate is purely cosmetic. correctCount
    is a plain JavaScript variable living in the browser tab. The
    if (correctCount >= 500) check that removes the blur filter runs
    entirely client-side — there's no server-side verification that 500
    problems were actually solved before the image becomes visible.

  2. The image itself is never gated at all. loadSecretImage() runs
    unconditionally on page load — before any quiz problems are even
    answered — and fetches the full, unblurred image bytes straight from
    /api/v1/internal/fetch-config-blob. The CSS filter: blur(20px)
    is applied only to the <img> element's rendering in the browser;
    the actual image data sitting in the DOM (and in the network
    response) was never blurred in the first place. Solving the quiz is
    a complete red herring — the blur is a display-only effect layered
    on top of a fully-formed image.

Exploit

No quiz-solving, no JavaScript execution, no exploitation required —
just fetch the endpoint directly and skip the browser entirely:

$ curl https://broncoctf-unblur-me.chals.io/api/v1/internal/fetch-config-blob -o out
$ file out
out: PNG image data, 1648 x 928, 8-bit/color RGBA, non-interlaced
Enter fullscreen mode Exit fullscreen mode

Opening the resulting pic.png shows the flag directly, rendered as
text on a graphic — no CSS blur applied to the actual file, since the
blur only ever existed as a browser-side rendering filter.

Flag

Reading the flag text off the image:

bronco{1_wouldnt_m@k3_you_do_c@lculus}
Enter fullscreen mode Exit fullscreen mode

Root Cause Summary

Issue Detail
Client-side-only access control The "solve 500 derivatives" requirement is enforced entirely in browser JavaScript (a simple counter variable), with no corresponding check on the server. Anyone can bypass it by simply not running the page's JS at all.
Security theater via CSS The image is never actually obscured at the data level — filter: blur() is a visual CSS effect applied only within the rendered page. The underlying bytes returned by the API are the full, clear image regardless of quiz progress.
Naming leak The endpoint path /api/v1/internal/fetch-config-blob and the flag's own text both hint that this asset was never meant to require 500 correct answers — the puzzle is the misdirection, not a real barrier.

Key Takeaways

  • Never trust logic that runs entirely in the client. Any gate, counter, or "requirement" implemented purely in JavaScript running in the user's own browser can be bypassed by simply not using that browser code path — e.g., calling the underlying API directly with curl.
  • CSS effects are not security controls. A blur(), display:none, visibility:hidden, or similar purely-visual CSS property does nothing to the underlying data — it only changes how a compliant browser renders it. The real data (and the request that fetched it) is fully accessible outside that rendering pipeline.
  • When a page implies a resource is "locked" behind some in-browser challenge, always check the Network tab (or just fetch known/guessable endpoints directly) — often the "lock" only exists in the UI layer.

Top comments (0)