KnightCTF 2026 - Web 100 - Knight Shop

A client-side coupon validation vulnerability in a ReactJS medieval shop app. The fix is as simple as clearing a cookie.

Task

A modern e-commerce platform for medieval equipment. I know you'll figure it out.

http://23.239.26.112:8087/

Flag Format: KCTF{Fl4g_heR3}

Enumeration

whatweb http://23.239.26.112:8087
# http://23.239.26.112:8087 [200 OK] ... Title[Knight Shop - Premium Medieval Equipment], X-Powered-By[Express]

Viewing the page source reveals a single JS import, confirming this is a ReactJS app:

<script defer="defer" src="/static/js/main.b42977dd.js"></script>

Since the JS bundle is minified, we can try fetching the source map, which is often left behind after a production build:

wget http://23.239.26.112:8087/static/js/main.b42977dd.js.map

b274505293ae2b8b1cbd369ccea2b860.png

The map file gives us all the original source files. The interesting ones are App.js and vendor.js. Opening them requires cleaning up escaped characters — in Neovim:

:%s/\\n//g
:%s/\\"/"/g

Vulnerability

Searching App.js for the string "flag" leads to the checkout handler:

const handleCheckout = async () => {
  const res = await fetch('/api/checkout', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      discountCode: discountCount > 0 ? coupon : '',
      discountCount: discountCount
    })
  });

  const data = await res.json();
  if (res.ok && data.flag) {
    setMessage(`Purchase successful! Your flag: ${data.flag}`);
  }
};

The flag is returned server-side on a successful purchase. The coupon logic lives in applyCoupon, which calls processTransaction imported from vendor.js:

export function processTransaction(input) {
  const result = _0x1a8c(input);
  return result;
}

function _0x1a8c(input) {
  const base = [75, 78, 73, 71, 72, 84];   // ASCII: K N I G H T
  const suffix = [50, 53];                  // ASCII: 2 5

  // validates coupon == "KNIGHT25"
  // if valid, sets cookie promo_applied=1 to prevent reuse
  document.cookie = 'promo_applied=1; path=/';
  return { valid: true, code: input };
}

The coupon code is KNIGHT25. The reuse prevention is handled entirely client-side via a cookie — which means we can just delete it and reapply as many times as we want.

Exploitation

First, create an account and add an item to the cart:

aa8e6936e0a3dbd74def96680c8ae214.png

a42a257330ce2c6d51d4a1ce433296db.png

Apply coupon KNIGHT25 — the 25% discount still isn't enough to cover the balance:

514208b8adc53c06be123441054240bb.png

Trying to apply it again gives Invalid Coupon code because the promo_applied cookie is now set:

023a695faa6cb166c317e6ea5380f1b7.png

Clear the cookie from the browser's developer tools and reapply. Repeat until the balance is sufficient:

3ccbba232e6f4a7d1ef5017b2ad11492.png

Proceed to checkout and the flag is returned in the response:

b1524d955909d3b3092b433fbd5b13c4.png

Flag

Flag: KCTF{kn1ght_c0up0n_m4st3r_2026}