I participated DiceCTF last week, it was quite fun! Didn’t expect it was this difficult..

Here is some of the challenge writeups I think quite special:

Challenges

knock-knock

Description

knock

Files

This challenge is quite special, it maybe appeared in real application, it show us how a simple mistake can lead to serious bug

First goto the link provided https://knock-knock.mc.ax/:

image1

Then tried to create a paste:

image2

Everything seems fine, check the source code:

const crypto = require('crypto');

class Database {
  constructor() {
    this.notes = [];
    this.secret = `secret-${crypto.randomUUID}`;
  }

  createNote({ data }) {
    const id = this.notes.length;
    this.notes.push(data);
    return {
      id,
      token: this.generateToken(id),
    };
  }

  getNote({ id, token }) {
    if (token !== this.generateToken(id)) return { error: 'invalid token' };
    if (id >= this.notes.length) return { error: 'note not found' };
    return { data: this.notes[id] };
  }

  generateToken(id) {
    return crypto
      .createHmac('sha256', this.secret)
      .update(id.toString())
      .digest('hex');
  }
}

const db = new Database();
db.createNote({ data: process.env.FLAG });

const express = require('express');
const app = express();

app.use(express.urlencoded({ extended: false }));
app.use(express.static('public'));

app.post('/create', (req, res) => {
  const data = req.body.data ?? 'no data provided.';
  const { id, token } = db.createNote({ data: data.toString() });
  res.redirect(`/note?id=${id}&token=${token}`);
});

app.get('/note', (req, res) => {
  const { id, token } = req.query;
  const note = db.getNote({
    id: parseInt(id ?? '-1'),
    token: (token ?? '').toString(),
  });
  if (note.error) {
    res.send(note.error);
  } else {
    res.send(note.data);
  }
});

app.listen(3000, () => {
  console.log('listening on port 3000');
});

After checking the source code, we can see the flag is put as the first note (id=0)

But how are we able to get the first note without the token?

Because the token is generated with HMAC with random string: crypto.randomUUID

Trying Prototype pollution

Then I stucked for few hours trying to exploit prototype pollution like this writeup but failed 😂

Build in docker

Eventually I ran up of ideas, so I decided to build this inside docker and investigate more

Before build it we need to run install the libraries:

Run npm init:

npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (knock)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to /home/hong/ctf/dicectf/knock/package.json:

{
  "name": "knock",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}


Is this OK? (yes)

Then run npm install express:

npm install express

added 50 packages, and audited 51 packages in 2s

2 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

After that, we can build it by docker build -t knock ., then run it using docker -d --name knock --rm -p 8080:3000 knock

Solving

After some experiment, notice every time I restart the token value is the same!!

That means we can predict the token when id=0 and get the flag!!

Then I get the token in the docker container:

docker exec -it knock bash
node@a0343b6e5f5c:/app$ ls
Dockerfile  index.js  node_modules  package-lock.json  package.json  yarn.lock
node@a0343b6e5f5c:/app$ node
Welcome to Node.js v17.4.0.
Type ".help" for more information.
> class Database {
...   constructor() {
.....     this.notes = [];
.....     this.secret = `secret-${crypto.randomUUID}`;
.....   }
...
...   createNote({ data }) {
...     const id = this.notes.length;
...     this.notes.push(data);
...     return {
.....       id,
.....       token: this.generateToken(id),
.....     };
...   }
...
...   getNote({ id, token }) {
...     if (token !== this.generateToken(id)) return { error: 'invalid token' };
...     if (id >= this.notes.length) return { error: 'note not found' };
...     return { data: this.notes[id] };
...   }
...
...   generateToken(id) {
...     return crypto
...       .createHmac('sha256', this.secret)
...       .update(id.toString())
...       .digest('hex');
...   }
... }
undefined
>
> const db = new Database();
undefined
> db.generateToken(0);
'7bd881fe5b4dcc6cdafc3e86b4a70e07cfd12b821e09a81b976d451282f6e264'

Then goto https://knock-knock.mc.ax/note?id=0&token=7bd881fe5b4dcc6cdafc3e86b4a70e07cfd12b821e09a81b976d451282f6e264 and get the freaking flag!!

Flag

dice{1_d00r_y0u_d00r_w3_a11_d00r_f0r_1_d00r}

After the competition notice the crypto.randomUUID in secret is not called, therefore it is static everytime:

db.secret
'secret-function randomUUID(options) {\n' +
  '  if (options !== undefined)\n' +
  "    validateObject(options, 'options');\n" +
  '  const {\n' +
  '    disableEntropyCache = false,\n' +
  '  } = options || {};\n' +
  '\n' +
  "  validateBoolean(disableEntropyCache, 'options.disableEntropyCache');\n" +
  '\n' +
  '  return disableEntropyCache ? getUnbufferedUUID() : getBufferedUUID();\n' +
  '}'

flagle

Description

flagle

Goto the link given https://flagle.mc.ax/

image1

As you can see, we need to “guess” the flag, each box can type 5 characters:

image2

And we know the flag starts with dice{, looks like when guess correctly it turns into green:

image3

Lets analyse the source:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Flagle</title>
    <link rel="stylesheet" href="style.css"/>
</head>

<body>
<div class="game-container">
    <div class="guess" id="guess-1">
        <input class="letter" data-guess="1" data-letter="1" maxlength="5" disabled>
        <input class="letter" data-guess="1" data-letter="2" maxlength="5" disabled>
        <input class="letter" data-guess="1" data-letter="3" maxlength="5" disabled>
        <input class="letter" data-guess="1" data-letter="4" maxlength="5" disabled>
        <input class="letter" data-guess="1" data-letter="5" maxlength="5" disabled>
        <input class="letter" data-guess="1" data-letter="6" maxlength="5" disabled>
    </div>
    <div class="guess" id="guess-2">
        <input class="letter" data-guess="2" data-letter="1" maxlength="5" disabled>
        <input class="letter" data-guess="2" data-letter="2" maxlength="5" disabled>
        <input class="letter" data-guess="2" data-letter="3" maxlength="5" disabled>
        <input class="letter" data-guess="2" data-letter="4" maxlength="5" disabled>
        <input class="letter" data-guess="2" data-letter="5" maxlength="5" disabled>
        <input class="letter" data-guess="2" data-letter="6" maxlength="5" disabled>
    </div>
    <div class="guess" id="guess-3">
        <input class="letter" data-guess="3" data-letter="1" maxlength="5" disabled>
        <input class="letter" data-guess="3" data-letter="2" maxlength="5" disabled>
        <input class="letter" data-guess="3" data-letter="3" maxlength="5" disabled>
        <input class="letter" data-guess="3" data-letter="4" maxlength="5" disabled>
        <input class="letter" data-guess="3" data-letter="5" maxlength="5" disabled>
        <input class="letter" data-guess="3" data-letter="6" maxlength="5" disabled>
    </div>
    <div class="guess" id="guess-4">
        <input class="letter" data-guess="4" data-letter="1" maxlength="5" disabled>
        <input class="letter" data-guess="4" data-letter="2" maxlength="5" disabled>
        <input class="letter" data-guess="4" data-letter="3" maxlength="5" disabled>
        <input class="letter" data-guess="4" data-letter="4" maxlength="5" disabled>
        <input class="letter" data-guess="4" data-letter="5" maxlength="5" disabled>
        <input class="letter" data-guess="4" data-letter="6" maxlength="5" disabled>
    </div>
    <div class="guess" id="guess-5">
        <input class="letter" data-guess="5" data-letter="1" maxlength="5" disabled>
        <input class="letter" data-guess="5" data-letter="2" maxlength="5" disabled>
        <input class="letter" data-guess="5" data-letter="3" maxlength="5" disabled>
        <input class="letter" data-guess="5" data-letter="4" maxlength="5" disabled>
        <input class="letter" data-guess="5" data-letter="5" maxlength="5" disabled>
        <input class="letter" data-guess="5" data-letter="6" maxlength="5" disabled>
    </div>
    <div class="guess" id="guess-6">
        <input class="letter" data-guess="6" data-letter="1" maxlength="5" disabled>
        <input class="letter" data-guess="6" data-letter="2" maxlength="5" disabled>
        <input class="letter" data-guess="6" data-letter="3" maxlength="5" disabled>
        <input class="letter" data-guess="6" data-letter="4" maxlength="5" disabled>
        <input class="letter" data-guess="6" data-letter="5" maxlength="5" disabled>
        <input class="letter" data-guess="6" data-letter="6" maxlength="5" disabled>
    </div>

    <button id="guess-button">GUESS</button>
</div>

<script src="flag-checker.js"></script>
<script src="script.js"></script>
<script>
    const guess = Module.cwrap('guess', 'number', ['number', 'string']);
    const CORRECT = 0;
    const WRONG_LOCATION = 1;
    const INCORRECT = 2;

    const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&()*+,-./:;<=>?@[]^_{|}~";

    const get_input = (guess_idx, letter_idx) => {
        return document.querySelector(`[data-guess="${guess_idx}"][data-letter="${letter_idx}"]`)
    };

    const keydown_listener = (e) => {
        const target = e.target;
        const guess_idx = +target.dataset.guess;
        const letter_idx = +target.dataset.letter;

        if (e.modifiers?.length > 0) {
            return;
        }

        if (e.key.length === 1 && !alphabet.includes(e.key)) {
            e.preventDefault();
            return false;
        }

        if (e.key === "Backspace" && target.value.length === 0 && letter_idx > 1) {
            get_input(guess_idx, letter_idx - 1).focus();
        } else if (e.key === "Delete" && target.value.length === 0 && letter_idx < 6) {
            const elem = get_input(guess_idx, letter_idx + 1);
            elem.focus();
            elem.setSelectionRange(0, 0);
            e.preventDefault();
            return false;
        } else if (e.key.length === 1 && target.value.length === 5 && letter_idx < 6) {
            get_input(guess_idx, letter_idx + 1).focus();
        } else if (e.key === "Enter") {
            submit_guess();
        }
    };

    let current_guess = 1;
    const guess_button = document.getElementById('guess-button');

    const submit_guess = () => {
        let correct = 0;
        let input_text = '';
        for (let i = 1; i <= 6; ++i) {
            const el = get_input(current_guess, i);

            const guess_val = el.value;
            input_text += guess_val;

            const result = guess(i, guess_val);
            console.log(result, guess_val, i, guess_val);
            if (result === CORRECT) {
                el.classList.add('correct');
                if (current_guess < 6) {
                    const next = get_input(current_guess + 1, i);
                    next.value = guess_val;
                    next.classList.add('correct');
                }
                correct++;
            } else if (result === WRONG_LOCATION) {
                el.classList.add('partial');
            } else if (result === INCORRECT) {
                el.classList.add('incorrect');
            }

            el.disabled = true;
            el.removeEventListener('keydown', keydown_listener);
        }
        current_guess++;

        if (correct === 6) {
            prompt('Congrats! Here\'s your flag:', input_text);
        }

        if (current_guess > 6) {
            guess_button.disabled = true;
        } else {
            init_guess();
        }
    };
    guess_button.onclick = submit_guess;

    const init_guess = () => {
        for (let i = 1; i <= 6; ++i) {
            const el = get_input(current_guess, i);
            if (!el.classList.contains('correct'))
                el.disabled = false;
            el.addEventListener('keydown', keydown_listener);
        }
    };
    init_guess();
</script>

</body>

</html>

The main code in the submit_guess function:

const submit_guess = () => {
    let correct = 0;
    let input_text = '';
    for (let i = 1; i <= 6; ++i) {
        const el = get_input(current_guess, i);

        const guess_val = el.value;
        input_text += guess_val;

        const result = guess(i, guess_val);
        console.log(result, guess_val, i, guess_val);
        if (result === CORRECT) {
            el.classList.add('correct');
            if (current_guess < 6) {
                const next = get_input(current_guess + 1, i);
                next.value = guess_val;
                next.classList.add('correct');
            }
            correct++;
        } else if (result === WRONG_LOCATION) {
            el.classList.add('partial');
        } else if (result === INCORRECT) {
            el.classList.add('incorrect');
        }

        el.disabled = true;
        el.removeEventListener('keydown', keydown_listener);
    }
    current_guess++;

    if (correct === 6) {
        prompt('Congrats! Here\'s your flag:', input_text);
    }

    if (current_guess > 6) {
        guess_button.disabled = true;
    } else {
        init_guess();
    }
};

As you can see, our input is pass into function guess, then it checks the return value equals CORRECT

If “guess” all correctly 6 times we got the flag

Decompile WASM file

When viewing the source we saw a flag-check.wasm file (WebAssembly file)

It is difficult to read, so we need to decompile it first

I used this tool from github: https://github.com/WebAssembly/wabt

Download the file, then run wasm-decompile flag-checker.wasm -o flag-check.c this will decompile the WASM code to C code

Checking the decompiled code you can see the guess function in here:

export function guess(a:int, b:int):int {
  var c:int = g_a - 16;
  g_a = c;
  var d:int = 2;
  if (f_k(b) != 5) goto B_a;
  if (eqz(streq(b, 1024))) goto B_b;
  d = a != 1;
  goto B_a;
  label B_b:
  var e:int = b[4]:ubyte;
  d = b[3]:ubyte;
  var f:int = b[2]:ubyte;
  var g:int = b[1]:ubyte;
  c[11]:byte = b[0]:ubyte;
  c[10]:byte = g;
  c[9]:byte = f;
  c[8]:byte = d;
  d = c[10]:ubyte;
  c[10]:byte = c[9]:ubyte;
  c[9]:byte = d;
  d = c[9]:ubyte;
  c[9]:byte = c[8]:ubyte;
  c[8]:byte = d;
  d = c[9]:ubyte;
  c[9]:byte = c[11]:ubyte;
  c[11]:byte = d;
  d = c[9]:ubyte;
  c[9]:byte = c[8]:ubyte;
  c[8]:byte = d;
  d = c[11]:ubyte;
  c[11]:byte = c[10]:ubyte;
  c[10]:byte = d;
  if (c[11]:ubyte != 51) goto B_c;
  if (c[10]:ubyte != 108) goto B_c;
  if (c[9]:ubyte != 33) goto B_c;
  d = c[8]:ubyte;
  if ((e & 255) != 68) goto B_c;
  if ((d & 255) != 70) goto B_c;
  d = a != 2;
  goto B_a;
  label B_c:
  f = b[1]:byte;
  if (f * (d = b[0]:byte) != 4800) goto B_d;
  g = b[2]:byte;
  if (g + d != 178) goto B_d;
  if (g + f != 126) goto B_d;
  if (g * (d = b[3]:byte) != 9126) goto B_d;
  if (d - (f = b[4]:byte) != 62) goto B_d;
  if (g * 4800 - f * d != 367965) goto B_d;
  d = a != 3;
  goto B_a;
  label B_d:
  if (eqz(env_validate_4(b))) goto B_e;
  d = a != 4;
  goto B_a;
  label B_e:
  var h:int = b[4]:ubyte;
  g = b[3]:byte;
  e = b[2]:byte;
  f = b[1]:byte;
  c[15]:byte = (b = b[0]:byte);
  c[14]:byte = f;
  c[13]:byte = e;
  c[12]:byte = g;
  c[15]:byte = c[15]:ubyte + 12;
  c[14]:byte = c[14]:ubyte + 4;
  c[13]:byte = c[13]:ubyte + 6;
  c[12]:byte = c[12]:ubyte + 2;
  if (c[15]:ubyte != 121) goto B_f;
  if (c[14]:ubyte != 68) goto B_f;
  if (c[13]:ubyte != 126) goto B_f;
  d = c[12]:ubyte;
  if ((h & 255) != 77) goto B_f;
  if ((d & 255) != 35) goto B_f;
  d = a != 5;
  goto B_a;
  label B_f:
  d = 2;
  if ((f + 2933) * (b + 1763) != 5483743) goto B_a;
  if ((h & 255) != 125) goto B_a;
  if ((g + 1546) * (e + 3913) != 6431119) goto B_a;
  d = a != 6;
  label B_a:
  g_a = c + 16;
  return d;
}

Guess the flag

The guess function is abit hard to read, luckily it provided in other function like the first validation in validate_1:

export function validate_1(a:int):int {
  return streq(a, 1024)
}

streq should be string equal I guess?

It compare the our input to 1024?

Nope, it refer to an address in the code here:

data d_a(offset: 1024) = "dice{\00";

Therefore the first validation is comparing the flag format dice{

Second

Second validation abit compilcated:

function validate(a:int, b:int, c:int, d:int, e:int):int {
  var f:int = g_a - 16;
  f[15]:byte = a;
  f[14]:byte = b;
  f[13]:byte = c;
  f[12]:byte = d;
  d = f[14]:ubyte;
  f[14]:byte = f[13]:ubyte;
  f[13]:byte = d;
  //02134
  d = f[13]:ubyte;
  f[13]:byte = f[12]:ubyte;
  f[12]:byte = d;
  //02314
  d = f[13]:ubyte;
  f[13]:byte = f[15]:ubyte;
  f[15]:byte = d;
  //32014
  d = f[13]:ubyte;
  f[13]:byte = f[12]:ubyte;
  f[12]:byte = d;
  //32104
  d = f[15]:ubyte;
  f[15]:byte = f[14]:ubyte;
  f[14]:byte = d;
  //23104
  d = 0;
  if (f[15]:ubyte != 51) goto B_a;
  if (f[14]:ubyte != 108) goto B_a;
  if (f[13]:ubyte != 33) goto B_a;
  d = e == 68 & f[12]:ubyte == 70;
  label B_a:
  return d;
}

It just wrap our input into different position and compare it with (51,108,33,70,68) which is ASCII

But I failed to reverse the position of the input, so I decided to brute force it…

First I use python to generate all possible position of the 5 characters:

Python 3.8.5 (default, Jan 27 2021, 15:41:15)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> bytearray([51,108,33,70,68])
bytearray(b'3l!FD')
>>> from itertools import permutations
>>> for l in list(permutations('3l!FD')):
...     print('"'+''.join(l)+'"',end=',')
...
"3l!FD","3l!DF","3lF!D","3lFD!","3lD!F","3lDF!","3!lFD","3!lDF","3!FlD","3!FDl","3!DlF","3!DFl","3Fl!D","3FlD!","3F!lD","3F!Dl","3FDl!","3FD!l","3Dl!F","3DlF!","3D!lF","3D!Fl","3DFl!","3DF!l","l3!FD","l3!DF","l3F!D","l3FD!","l3D!F","l3DF!","l!3FD","l!3DF","l!F3D","l!FD3","l!D3F","l!DF3","lF3!D","lF3D!","lF!3D","lF!D3","lFD3!","lFD!3","lD3!F","lD3F!","lD!3F","lD!F3","lDF3!","lDF!3","!3lFD","!3lDF","!3FlD","!3FDl","!3DlF","!3DFl","!l3FD","!l3DF","!lF3D","!lFD3","!lD3F","!lDF3","!F3lD","!F3Dl","!Fl3D","!FlD3","!FD3l","!FDl3","!D3lF","!D3Fl","!Dl3F","!DlF3","!DF3l","!DFl3","F3l!D","F3lD!","F3!lD","F3!Dl","F3Dl!","F3D!l","Fl3!D","Fl3D!","Fl!3D","Fl!D3","FlD3!","FlD!3","F!3lD","F!3Dl","F!l3D","F!lD3","F!D3l","F!Dl3","FD3l!","FD3!l","FDl3!","FDl!3","FD!3l","FD!l3","D3l!F","D3lF!","D3!lF","D3!Fl","D3Fl!","D3F!l","Dl3!F","Dl3F!","Dl!3F","Dl!F3","DlF3!","DlF!3","D!3lF","D!3Fl","D!l3F","D!lF3","D!F3l","D!Fl3","DF3l!","DF3!l","DFl3!","DFl!3","DF!3l","DF!l3",>>>

Then brute force it in inspect console tab:

var inp = ["3l!FD","3l!DF","3lF!D","3lFD!","3lD!F","3lDF!","3!lFD","3!lDF","3!FlD","3!FDl","3!DlF","3!DFl","3Fl!D","3FlD!","3F!lD","3F!Dl","3FDl!","3FD!l","3Dl!F","3DlF!","3D!lF","3D!Fl","3DFl!","3DF!l","l3!FD","l3!DF","l3F!D","l3FD!","l3D!F","l3DF!","l!3FD","l!3DF","l!F3D","l!FD3","l!D3F","l!DF3","lF3!D","lF3D!","lF!3D","lF!D3","lFD3!","lFD!3","lD3!F","lD3F!","lD!3F","lD!F3","lDF3!","lDF!3","!3lFD","!3lDF","!3FlD","!3FDl","!3DlF","!3DFl","!l3FD","!l3DF","!lF3D","!lFD3","!lD3F","!lDF3","!F3lD","!F3Dl","!Fl3D","!FlD3","!FD3l","!FDl3","!D3lF","!D3Fl","!Dl3F","!DlF3","!DF3l","!DFl3","F3l!D","F3lD!","F3!lD","F3!Dl","F3Dl!","F3D!l","Fl3!D","Fl3D!","Fl!3D","Fl!D3","FlD3!","FlD!3","F!3lD","F!3Dl","F!l3D","F!lD3","F!D3l","F!Dl3","FD3l!","FD3!l","FDl3!","FDl!3","FD!3l","FD!l3","D3l!F","D3lF!","D3!lF","D3!Fl","D3Fl!","D3F!l","Dl3!F","Dl3F!","Dl!3F","Dl!F3","DlF3!","DlF!3","D!3lF","D!3Fl","D!l3F","D!lF3","D!F3l","D!Fl3","DF3l!","DF3!l","DFl3!","DFl!3","DF!3l","DF!l3"]

for (let index = 0; index < inp.length; index++) {
    if (guess(2,inp[index]) == CORRECT){
        console.log(inp[index]);
    }
}
// Output: F!3lD

Yay!! We found the 2nd part which is F!3lD

Third

export function validate_3(a:int, b:int, c:int, d:int, e:int):int {
  var f:int = 0;
  if (b * a != 4800) goto B_a;
  if (c + a != 178) goto B_a;
  if (c + b != 126) goto B_a;
  if (d * c != 9126) goto B_a;
  if (d - e != 62) goto B_a;
  f = c * 4800 - e * d == 367965;
  label B_a:
  return f;
}

As you can see, a is the first character of our input, b is second and so on..

So we need to somehow calculate the correct input?

But I was lazy, I used the z3 sat solver to help me:

from z3 import *
s = Solver()
# Declare 5 Bit Vector
p3 = [BitVec('b%i'%i,16) for i in range(5)]
# Set condition for each character
s.add(p3[0] * p3[1] == 4800)
s.add(p3[2] + p3[0] == 178)
s.add(p3[2] + p3[1] == 126)
s.add(p3[2] * p3[3] == 9126)
s.add(p3[3] - p3[4] == 62)
s.add(p3[2] * 4800 - p3[4] * p3[3] == 367965)
s.check()
model = s.model()
for p in p3:
  # Print the result
  print(chr(model[p].as_long()),end='')
# d0Nu7

Yay!! We calculate the 3rd part is d0Nu7

Fourth

The fourth part is from the script.js but it was obfuscated:

function c(b) {
    var e = {
        'HLPDd': function(g, h) {
            return g === h;
        },
        'tIDVT': function(g, h) {
            return g(h);
        },
        'QIMdf': function(g, h) {
            return g - h;
        },
        'FIzyt': 'int',
        'oRXGA': function(g, h) {
            return g << h;
        },
        'AMINk': function(g, h) {
            return g & h;
        }
    }
      , f = current_guess;
    try {
        let g = e['HLPDd'](btoa(e['tIDVT'](intArrayToString, window[b](b[e['QIMdf'](f, 0x26f4 + 0x1014 + -0x3707 * 0x1)], e['FIzyt'])()['toString'](e['oRXGA'](e['AMINk'](f, -0x1a3 * -0x15 + 0x82e * -0x1 + -0x1a2d), 0x124d + -0x1aca + 0x87f))['match'](/.{2}/g)['map'](h=>parseInt(h, f * f)))), 'ZGljZQ==') ? -0x1 * 0x1d45 + 0x2110 + -0x3ca : -0x9 * 0x295 + -0x15 * -0x3 + 0x36 * 0x6d;
    } catch {
        return 0x1b3c + -0xc9 * 0x2f + -0x19 * -0x63;
    }
}

I stuck here for a long time.. The cleaned code looks like this:

intArrayToString(window[b](b[f-1], 'int')()['toString'](f&4 << 2)['match'](/.{2}/g)['map'](h=>parseInt(h, f * f))) === 'dice' ? 1 : 0;

Basically take one character of our input and do some convertion and see if equal to dice

Then I notice it pass our input as attribute of window because b is our input

Therefore I decided list all the window’s attributes with 5 characters:

let keys = Object.keys(window)
undefined
for (let index = 0; index < keys.length ; index++) {
    if(keys[index].length == 5) console.log(keys[index]);
}
alert
close
fetch
focus
print
quit_
read_
IDBFS
ABORT
ccall
cwrap
_free
HEAP8
abort

Then I brute force one by one, turns out cwrap is the correct input!

Fifth

export function validate_5(a:int, b:int, c:int, d:int, e:int):int {
  var f:int = g_a - 16;
  f[15]:byte = a;
  f[14]:byte = b;
  f[13]:byte = c;
  f[12]:byte = d;
  f[15]:byte = f[15]:ubyte + 12;
  f[14]:byte = f[14]:ubyte + 4;
  f[13]:byte = f[13]:ubyte + 6;
  f[12]:byte = f[12]:ubyte + 2;
  d = 0;
  if (f[15]:ubyte != 121) goto B_a;
  if (f[14]:ubyte != 68) goto B_a;
  if (f[13]:ubyte != 126) goto B_a;
  d = e == 77 & f[12]:ubyte == 35;
  label B_a:
  return d;
}

This was easy, just minus the value and print it:

p5 = bytearray([121-12,68-4,126-6,35-2,77])
print(p5)
# bytearray(b'm@x!M')

Last

export function validate_6(a:int, b:int, c:int, d:int, e:int):int {
  var f:int = 0;
  if ((b + 2933) * (a + 1763) != 5483743) goto B_a;
  f = e == 125 & (d + 1546) * (c + 3913) == 6431119;
  label B_a:
  return f;
}

Same as the third part, I used Z3 sat solver to get the correct input:

s2 = Solver()
# Declare 5 Bit Vector
p6 = [BitVec('c%i'%i,16) for i in range(5)]
for i in range(5):
  # Make sure the input is printable character
  s2.add(And(p6[i] <= 126, p6[i] >= 33))
# Set condition for each character
s2.add((p6[1] + 2933) * (p6[0] + 1763) == 5483743)
s2.add(p6[4] == 125)
s2.add((p6[3] + 1546) * (p6[2] + 3913) == 6431119)
s2.check()
model = s2.model()
for p in p6:
  # Print the result
  print(chr(model[p].as_long()),end='')
# T$r3}

And lastly we get the last part T$r3}

Full solve script here

Enter the flag in the website, on the fourth time it say it is the correct flag!!

image4

Flag

dice{F!3lDd0Nu7cwrapm@x!MT$r3}