siunam's Website

My personal website

Home Writeups Blog Projects About E-Portfolio

Catch-22

Overview

Background

You need a key to open the door… but what if the key is in the room?

http://chal-a.hkcert22.pwnable.hk:28251 , http://chal-b.hkcert22.pwnable.hk:28251

Attachment: catch-22_c445efdb7185cb4c1a7b3002462179d6.zip

Solution: https://hackmd.io/@blackb6a/hkcert-ctf-2022-ii-en-6a196795

Find the flag

In this challenge, we can download an attachment:

┌──(root🌸siunam)-[~/ctf/HKCERT-CTF-2022/Crypto/Catch-22]
└─# unzip catch-22_c445efdb7185cb4c1a7b3002462179d6.zip           
Archive:  catch-22_c445efdb7185cb4c1a7b3002462179d6.zip
  inflating: package.json            
   creating: src/
  inflating: src/app.js              
   creating: src/views/
  inflating: src/views/home.handlebars  
   creating: src/views/layouts/
  inflating: src/views/layouts/main.handlebars  
  inflating: src/views/register.handlebars  
  inflating: src/util.js             
  inflating: src/actions.js          
  inflating: src/constants.js        
  inflating: yarn.lock

Home page:

Let’s register an account for testing!

In here, we can see that the map has 2 keys, 1 key is reachable, and has 3 locked door?

That’s impossible to unlock all the door in a normal way!

Let’s look at the source code!

util.js:

const crypto = require('crypto')

const key = crypto.randomBytes(16)

function encryptToken (state) {
  const token = JSON.stringify(state)
  const cipher = crypto.createCipheriv('aes-128-ecb', key, null)
  cipher.setAutoPadding(true)
  const encryptedToken = Buffer.concat([
    cipher.update(token),
    cipher.final()
  ])
  return encryptedToken.toString('hex')
}

function decryptToken (encryptedTokenHex) {
  const encryptedToken = Buffer.from(encryptedTokenHex, 'hex')
  const cipher = crypto.createDecipheriv('aes-128-ecb', key, null)
  cipher.setAutoPadding(true)
  const token = Buffer.concat([
    cipher.update(encryptedToken),
    cipher.final()
  ]).toString()
  const state = JSON.parse(token)
  return { token, state }
}

module.exports = {
  encryptToken,
  decryptToken
}

Hmm… We can see the token is being decrypted and encrypted via AES ECB mode.

app.js:

[...]
app.post('/register', bodyParser.urlencoded(), function (req, res) {
  try {
    const { username } = req.body

    const newToken = encryptToken({
      username,
      x: 13,
      y: 5,
      inventory: [],
      onMapItems: [
        {item: ITEMS.KEY, x: 3, y: 4},
        {item: ITEMS.DOOR, x: 4, y: 5},
        {item: ITEMS.DOOR, x: 5, y: 5},
        {item: ITEMS.DOOR, x: 6, y: 5},
        {item: ITEMS.KEY, x: 15, y: 1},
      ]
    })

    res.cookie('game-token', newToken)
    return res.redirect('/')
  } catch (err) {
    console.error(err)
    return res.status(500).json({ error: 'unexpected error' })
  }
})
[...]

In app.js, when we send a POST request in /register, it’ll assign us a token, and parse it to set a cookie.

decryptToken:

{"username":"siunam","x":13,"y":5,"inventory":[],"onMapItems":[{"item":0,"x":3,"y":4},{"item":1,"x":4,"y":5},{"item":1,"x":5,"y":5},{"item":1,"x":6,"y":5},{"item":0,"x":15,"y":1}]}

Cookies:

aca1fae0ede7f469260b38ab325fc0a7c0a578b67ff8e93a7ca255c1802b5178d092ffc0b18edeb060f3fc660a8d84abcf4f7a9b0dccc8ae94e09439f208e9e06ed7d7778ae3d0b1ded2363941565dae601cd81f55858da6fff04a27e13d8f435b7c4b8abbb05ecc99b4679ba42b5538d48ffbe7581c43f177590f71650580b5dfe0cd22903b61749a6b151e921fe4f8e1350780de45150a9fcfb910b4479a4a2c58fc700f9be2af04a501028627908aa6087df656b66d2d2ca83810e594022e

Armed with the above information, we can try to dig much deeper!

Block ciphers, like AES (Advanced Encryption Standard), are only able to encrypt messages with a fixed length. In our instance, AES can only encrypt messages of 16 bytes.

AES electronic codebook (ECB) mode:

In this challenge, we can see in util.js, it’s using ECB mode, and each message block is encrypted by every 16 bytes.

Let’s look at our cookie value!

Translate each message block to 16 bytes:

Plaintext Block Ciphertext Block
{“username”:”siu aca1fae0ede7f469260b38ab325fc0a7
nam”,”x”:13,”y”: c0a578b67ff8e93a7ca255c1802b5178
5,”inventory”:[] d092ffc0b18edeb060f3fc660a8d84ab
,”onMapItems”:[{ cf4f7a9b0dccc8ae94e09439f208e9e0
“item”:0,”x”:3,” 6ed7d7778ae3d0b1ded2363941565dae
y”:4},{“item”:1, 601cd81f55858da6fff04a27e13d8f43
“x”:4,”y”:5},{“i 5b7c4b8abbb05ecc99b4679ba42b5538
tem”:1,”x”:5,”y” d48ffbe7581c43f177590f71650580b5
:5},{“item”:1,”x dfe0cd22903b61749a6b151e921fe4f8
“:6,”y”:5},{“ite e1350780de45150a9fcfb910b4479a4a
m”:0,”x”:15,”y”: 2c58fc700f9be2af04a501028627908a
1}]}____ a6087df656b66d2d2ca83810e594022e

Note: 16 bytes = 32 hex characters, _ = padding character

Also, since every blocks are independently encrypted, we can just modify one of those block!

What if I fill the inventory with bunch of keys by swaping the ciphertext block??

After I fumbling around, when I pick up a key, the inventory will add a value called 0.

To swap the ciphertext block, I’ll register an account named siu0,0,0,0,0,0,0,0 0nam, the ciphertext block will become:

Plaintext Block Ciphertext Block
{“username”:”siu aca1fae0ede7f469260b38ab325fc0a7
0,0,0,0,0,0,0,0 8c1842fb0df889fd6ed5416f7507a0b0
0nam”,”x”:13,”y” 5762c9742bf323de4593eee85eca5067
:5,”inventory”:[ f90d10d66852d845107a087525c48918
],”onMapItems”:[ 42924e9de696556ccd87cd2fc3521bb5
{“item”:0,”x”:3, a3b6abc3b51b9451c9830d89b581440c
“y”:4},{“item”:1 f7e8419ca8f122e0493ddfc213523685
,”x”:4,”y”:5},{“ 8922e4617b39d6b06abdbb05c8b1a619
item”:1,”x”:5,”y 5656e0d6c9f584984e11a2397e28bdd3
“:5},{“item”:1,” c19796e2c753f799143f2f5e314ff561
x”:6,”y”:5},{“it 90fdf086bfd8d8eb1f8076079eb2d15c
em”:0,”x”:15,”y” e3dd02803671e9359bef00fbc4936c1e
:1}]}{padding} d38069bc2385c77065fa5fe3781739a1

You can see that we have a new ciphertext block which filled with bunch of 0’s.

Now, what if I move the second ciphertext block (0’s) to between the fourth and the fifth blocks? If we successfully modify the ciphertext block, it’ll be:

Plaintext Block Ciphertext Block
{“username”:”siu aca1fae0ede7f469260b38ab325fc0a7
0nam”,”x”:13,”y” 5762c9742bf323de4593eee85eca5067
:5,”inventory”:[ f90d10d66852d845107a087525c48918
0,0,0,0,0,0,0,0 8c1842fb0df889fd6ed5416f7507a0b0
],”onMapItems”:[ 42924e9de696556ccd87cd2fc3521bb5
{“item”:0,”x”:3, a3b6abc3b51b9451c9830d89b581440c
“y”:4},{“item”:1 f7e8419ca8f122e0493ddfc213523685
,”x”:4,”y”:5},{“ 8922e4617b39d6b06abdbb05c8b1a619
item”:1,”x”:5,”y 5656e0d6c9f584984e11a2397e28bdd3
“:5},{“item”:1,” c19796e2c753f799143f2f5e314ff561
x”:6,”y”:5},{“it 90fdf086bfd8d8eb1f8076079eb2d15c
em”:0,”x”:15,”y” e3dd02803671e9359bef00fbc4936c1e
:1}]}{padding} d38069bc2385c77065fa5fe3781739a1

Which we’ll have 8 keys!

We can modify the cookie via document.cookie="game-token=<cookie_value_here>".

Modified cookie:

document.cookie="game-token=aca1fae0ede7f469260b38ab325fc0a75762c9742bf323de4593eee85eca5067f90d10d66852d845107a087525c489188c1842fb0df889fd6ed5416f7507a0b042924e9de696556ccd87cd2fc3521bb5a3b6abc3b51b9451c9830d89b581440cf7e8419ca8f122e0493ddfc2135236858922e4617b39d6b06abdbb05c8b1a6195656e0d6c9f584984e11a2397e28bdd3c19796e2c753f799143f2f5e314ff56190fdf086bfd8d8eb1f8076079eb2d15ce3dd02803671e9359bef00fbc4936c1ed38069bc2385c77065fa5fe3781739a1"

Let’s fire up the developer console and change our cookie!

Now, if we move once, we should see 8 keys in our inventory!

Yes!! Let’s unlock all the doors and get the flag!

We got the flag!

Conclusion

What we’ve learned:

  1. Cut-and-Paste Attack in AES ECB Mode